Skip to content

Commit

Permalink
feat: add twilio room status callback (#45)
Browse files Browse the repository at this point in the history
Signed-off-by: Dumbledore <mathenge362@gmail.com>
  • Loading branch information
ageeknamedslickback authored and NYARAS committed Aug 19, 2021
1 parent 8548a65 commit 3a8acfa
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 81 deletions.
5 changes: 5 additions & 0 deletions pkg/engagement/application/common/dto/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,8 @@ type MailgunEventOutput struct {
EventName string `json:"event" firestore:"event"`
DeliveredOn time.Time `json:"timestamp" firestore:"deliveredOn"`
}

// CallbackData records data sent back from the Twilio API to our HTTP callback URL
type CallbackData struct {
Values map[string][]string `json:"values,omitempty" firestore:"values,omitempty"`
}
74 changes: 29 additions & 45 deletions pkg/engagement/infrastructure/database/firebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/savannahghi/feedlib"
"github.com/savannahghi/firebasetools"
"github.com/sirupsen/logrus"
"gitlab.slade360emr.com/go/apiclient"
"go.opentelemetry.io/otel"

"github.com/savannahghi/engagement/pkg/engagement/application/common/helpers"
Expand All @@ -40,7 +39,6 @@ const (
messagesSubcollectionName = "messages"
incomingEventsCollectionName = "incoming_events"
outgoingEventsCollectionName = "outgoing_events"
marketingDataCollectionName = "marketing_data"
//AITMarketingMessageName is the name of a Cloud Firestore collection into which AIT
// callback data will be saved for future analysis
AITMarketingMessageName = "ait_marketing_sms"
Expand All @@ -50,6 +48,8 @@ const (

twilioCallbackCollectionName = "twilio_callbacks"

twilioVideoCallbackCollectionName = "twilio_video_callbacks"

notificationCollectionName = "notifications"

labelsDocID = "item_labels"
Expand Down Expand Up @@ -908,11 +908,6 @@ func (fr Repository) getTwilioCallbackCollectionName() string {
return suffixed
}

func (fr Repository) getMarketingDataCollectionName() string {
suffixed := firebasetools.SuffixCollection(marketingDataCollectionName)
return suffixed
}

func (fr Repository) getOutgoingEmailsCollectionName() string {
return firebasetools.SuffixCollection(outgoingEmails)
}
Expand Down Expand Up @@ -968,6 +963,11 @@ func (fr Repository) getMessagesCollection(
return messagesColl
}

func (fr Repository) getTwilioVideoCallbackCollectionName() string {
suffixed := firebasetools.SuffixCollection(twilioVideoCallbackCollectionName)
return suffixed
}

func (fr Repository) elementExists(
ctx context.Context,
collection *firestore.CollectionRef,
Expand Down Expand Up @@ -1809,44 +1809,6 @@ func (fr Repository) SaveNPSResponse(
return nil
}

// UpdateMessageSentStatus updates the message sent status to true
func (fr Repository) UpdateMessageSentStatus(
ctx context.Context,
phonenumber string,
segment string,
) error {
ctx, span := tracer.Start(ctx, "UpdateMessageSentStatus")
defer span.End()
query := fr.firestoreClient.Collection(fr.getMarketingDataCollectionName()).
Where("message_sent", "==", "FALSE").
Where("properties.Phone", "==", phonenumber).
Where("properties.InitialSegment", "==", segment)

docs, err := fetchQueryDocs(ctx, query, true)
if err != nil {
helpers.RecordSpanError(span, err)
return err
}

var marketingData apiclient.Segment
err = docs[0].DataTo(&marketingData)
if err != nil {
helpers.RecordSpanError(span, err)
return fmt.Errorf(
"unable to unmarshal marketing Data from doc snapshot: %w", err)
}

marketingData.MessageSent = "TRUE"

doc := fr.firestoreClient.Collection(fr.getMarketingDataCollectionName()).
Doc(docs[0].Ref.ID)
if _, err = doc.Set(ctx, marketingData); err != nil {
helpers.RecordSpanError(span, err)
return err
}
return nil
}

// SaveOutgoingEmails saves all the outgoing emails
func (fr Repository) SaveOutgoingEmails(
ctx context.Context,
Expand Down Expand Up @@ -1908,3 +1870,25 @@ func (fr Repository) UpdateMailgunDeliveryStatus(

return &emailLogData, nil
}

// SaveTwilioVideoCallbackStatus saves the callback data
func (fr Repository) SaveTwilioVideoCallbackStatus(
ctx context.Context,
data dto.CallbackData,
) error {
ctx, span := tracer.Start(ctx, "SaveTwilioVideoCallbackStatus")
defer span.End()
if err := fr.checkPreconditions(); err != nil {
helpers.RecordSpanError(span, err)
return fmt.Errorf("repository precondition check failed: %w", err)
}

collectionName := fr.getTwilioVideoCallbackCollectionName()
_, _, err := fr.firestoreClient.Collection(collectionName).Add(ctx, data)
if err != nil {
helpers.RecordSpanError(span, err)
return fmt.Errorf("unable to save callback response")
}

return nil
}
10 changes: 0 additions & 10 deletions pkg/engagement/infrastructure/services/sms/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,6 @@ func (s Service) GetMarketingSMSByPhone(
return s.Repository.GetMarketingSMSByPhone(ctx, phoneNumber)
}

// UpdateMessageSentStatus updates the message sent field to true when a message
// is sent to a user
func (s Service) UpdateMessageSentStatus(
ctx context.Context,
phonenumber string,
segment string,
) error {
return s.Repository.UpdateMessageSentStatus(ctx, phonenumber, segment)
}

// SendToMany is a utility method to send to many recipients at the same time
func (s Service) SendToMany(
ctx context.Context,
Expand Down
13 changes: 13 additions & 0 deletions pkg/engagement/infrastructure/services/twilio/mock/twilio_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ type FakeServiceTwilio struct {
TwilioAccessTokenFn func(ctx context.Context) (*dto.AccessToken, error)

SendSMSFn func(ctx context.Context, to string, msg string) error

SaveTwilioVideoCallbackStatusFn func(
ctx context.Context,
data dto.CallbackData,
) error
}

// MakeTwilioRequest ...
Expand All @@ -47,3 +52,11 @@ func (f *FakeServiceTwilio) TwilioAccessToken(ctx context.Context) (*dto.AccessT
func (f *FakeServiceTwilio) SendSMS(ctx context.Context, to string, msg string) error {
return f.SendSMSFn(ctx, to, msg)
}

// SaveTwilioVideoCallbackStatus ..
func (f *FakeServiceTwilio) SaveTwilioVideoCallbackStatus(
ctx context.Context,
data dto.CallbackData,
) error {
return f.SaveTwilioVideoCallbackStatusFn(ctx, data)
}
18 changes: 17 additions & 1 deletion pkg/engagement/infrastructure/services/twilio/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/savannahghi/engagement/pkg/engagement/application/common/dto"
"github.com/savannahghi/engagement/pkg/engagement/application/common/helpers"
"github.com/savannahghi/engagement/pkg/engagement/infrastructure/services/sms"
"github.com/savannahghi/engagement/pkg/engagement/repository"
"github.com/savannahghi/firebasetools"
"github.com/savannahghi/serverutils"
"go.opentelemetry.io/otel"
Expand Down Expand Up @@ -57,10 +58,15 @@ type ServiceTwilio interface {
TwilioAccessToken(ctx context.Context) (*dto.AccessToken, error)

SendSMS(ctx context.Context, to string, msg string) error

SaveTwilioVideoCallbackStatus(
ctx context.Context,
data dto.CallbackData,
) error
}

// NewService initializes a service to interact with Twilio
func NewService(sms sms.ServiceSMS) *Service {
func NewService(sms sms.ServiceSMS, repo repository.Repository) *Service {
region := serverutils.MustGetEnvVar(TwilioRegionEnvVarName)
videoBaseURL := serverutils.MustGetEnvVar(TwilioVideoAPIURLEnvVarName)
videoAPIKeySID := serverutils.MustGetEnvVar(TwilioVideoAPIKeySIDEnvVarName)
Expand All @@ -86,6 +92,7 @@ func NewService(sms sms.ServiceSMS) *Service {
callbackURL: callbackURL,
smsNumber: smsNumber,
sms: sms,
repository: repo,
}
srv.checkPreconditions()
return srv
Expand All @@ -105,6 +112,7 @@ type Service struct {
callbackURL string
smsNumber string
sms sms.ServiceSMS
repository repository.Repository
}

func (s Service) checkPreconditions() {
Expand Down Expand Up @@ -315,3 +323,11 @@ func (s Service) SendSMS(ctx context.Context, to string, msg string) error {
fmt.Printf("Raw Twilio SMS response: %v", t)
return nil
}

// SaveTwilioVideoCallbackStatus saves status callback data
func (s Service) SaveTwilioVideoCallbackStatus(
ctx context.Context,
data dto.CallbackData,
) error {
return s.repository.SaveTwilioVideoCallbackStatus(ctx, data)
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func newTwilioService(ctx context.Context) (*twilio.Service, error) {
crmExt := crmExt.NewCrmService(hubspotUsecases, mail)
sms := sms.NewService(repo, crmExt, ns, edi)

return twilio.NewService(sms), nil
return twilio.NewService(sms, repo), nil
}

func TestNewService(t *testing.T) {
Expand Down
9 changes: 6 additions & 3 deletions pkg/engagement/presentation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ func Router(ctx context.Context) (*mux.Router, error) {
sms := sms.NewService(fr, crmExt, ns, edi)
feed := usecases.NewFeed(fr, ns)
whatsapp := whatsapp.NewService()
twilio := twilio.NewService(sms)
otp := otp.NewService(whatsapp, mail, sms, twilio)
tw := twilio.NewService(sms, fr)
otp := otp.NewService(whatsapp, mail, sms, tw)
surveys := surveys.NewService(fr)

// Initialize the interactor
Expand All @@ -151,7 +151,7 @@ func Router(ctx context.Context) (*mux.Router, error) {
*mail,
whatsapp,
otp,
twilio,
tw,
fcm,
surveys,
hubspotService,
Expand Down Expand Up @@ -223,6 +223,9 @@ func Router(ctx context.Context) (*mux.Router, error) {
r.Path("/twilio_fallback").
Methods(http.MethodPost).
HandlerFunc(h.GetFallbackHandler())
r.Path(twilio.TwilioCallbackPath).
Methods(http.MethodPost).
HandlerFunc(h.GetTwilioVideoCallbackFunc())
r.Path("/facebook_data_deletion_callback").Methods(
http.MethodPost,
).HandlerFunc(h.DataDeletionRequestCallback())
Expand Down
22 changes: 22 additions & 0 deletions pkg/engagement/presentation/rest/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ type PresentationHandlers interface {
HubSpotFirestoreSync() http.HandlerFunc

DataDeletionRequestCallback() http.HandlerFunc

GetTwilioVideoCallbackFunc() http.HandlerFunc
}

// PresentationHandlersImpl represents the usecase implementation object
Expand Down Expand Up @@ -1961,3 +1963,23 @@ func (p PresentationHandlersImpl) DataDeletionRequestCallback() http.HandlerFunc
respondWithJSON(w, http.StatusOK, resp)
}
}

// GetTwilioVideoCallbackFunc generates a Twilio Video callback handling function.
//
// Twilio sends the data with the "Content-Type" header to “application/x-www-urlencoded”.
func (p PresentationHandlersImpl) GetTwilioVideoCallbackFunc() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Form == nil || len(r.Form) == 0 {
return
}
if err := p.interactor.Twilio.SaveTwilioVideoCallbackStatus(
r.Context(),
dto.CallbackData{Values: r.Form},
); err != nil {
log.Printf(
"Twilio video callback error: unable to save callback response: %v",
err,
)
}
}
}
28 changes: 13 additions & 15 deletions pkg/engagement/repository/mock/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,6 @@ type FakeEngagementRepository struct {
data *dto.MarketingSMS,
) (*dto.MarketingSMS, error)

UpdateMessageSentStatusFn func(
ctx context.Context,
phonenumber string,
segment string,
) error

UpdateUserCRMEmailFn func(ctx context.Context, phoneNumber string, payload *dto.UpdateContactPSMessage) error
UpdateUserCRMBewellAwareFn func(ctx context.Context, email string, payload *dto.UpdateContactPSMessage) error

Expand All @@ -271,6 +265,11 @@ type FakeEngagementRepository struct {
ctx context.Context,
phoneNumber string,
) (*dto.MarketingSMS, error)

SaveTwilioVideoCallbackStatusFn func(
ctx context.Context,
data dto.CallbackData,
) error
}

// GetFeed ...
Expand Down Expand Up @@ -591,15 +590,6 @@ func (f *FakeEngagementRepository) UpdateMarketingMessage(
return f.UpdateMarketingMessageFn(ctx, data)
}

// UpdateMessageSentStatus ..
func (f *FakeEngagementRepository) UpdateMessageSentStatus(
ctx context.Context,
phonenumber string,
segment string,
) error {
return f.UpdateMessageSentStatusFn(ctx, phonenumber, segment)
}

// UpdateUserCRMEmail ..
func (f *FakeEngagementRepository) UpdateUserCRMEmail(ctx context.Context, phoneNumber string, payload *dto.UpdateContactPSMessage) error {
return f.UpdateUserCRMEmailFn(ctx, phoneNumber, payload)
Expand Down Expand Up @@ -632,3 +622,11 @@ func (f *FakeEngagementRepository) GetMarketingSMSByID(
) (*dto.MarketingSMS, error) {
return f.GetMarketingSMSByIDFn(ctx, id)
}

// SaveTwilioVideoCallbackStatus ..
func (f *FakeEngagementRepository) SaveTwilioVideoCallbackStatus(
ctx context.Context,
data dto.CallbackData,
) error {
return f.SaveTwilioVideoCallbackStatusFn(ctx, data)
}
15 changes: 10 additions & 5 deletions pkg/engagement/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,17 @@ type Repository interface {
response *dto.NPSResponse,
) error

UpdateMessageSentStatus(
SaveOutgoingEmails(
ctx context.Context,
phonenumber string,
segment string,
payload *dto.OutgoingEmailsLog,
) error
UpdateMailgunDeliveryStatus(
ctx context.Context,
payload *dto.MailgunEvent,
) (*dto.OutgoingEmailsLog, error)

SaveOutgoingEmails(ctx context.Context, payload *dto.OutgoingEmailsLog) error
UpdateMailgunDeliveryStatus(ctx context.Context, payload *dto.MailgunEvent) (*dto.OutgoingEmailsLog, error)
SaveTwilioVideoCallbackStatus(
ctx context.Context,
data dto.CallbackData,
) error
}
2 changes: 1 addition & 1 deletion pkg/engagement/usecases/feed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func InitializeFakeEngagementInteractor() (*interactor.Interactor, error) {
crmExt := crmExt.NewCrmService(hubspotUsecases, mail)
sms := sms.NewService(r, crmExt, messagingSvc, ediSvc)
whatsapp := whatsapp.NewService()
twilio := twilio.NewService(sms)
twilio := twilio.NewService(sms, r)
otp := otp.NewService(whatsapp, mail, sms, twilio)
surveys := surveys.NewService(r)

Expand Down

0 comments on commit 3a8acfa

Please sign in to comment.