Skip to content

Commit

Permalink
feat: set up a review queue for autolinking failures
Browse files Browse the repository at this point in the history
  • Loading branch information
Salaton committed Aug 16, 2021
1 parent 2328e41 commit 27741e9
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 19 deletions.
16 changes: 16 additions & 0 deletions pkg/onboarding/application/dto/input.go
Expand Up @@ -447,3 +447,19 @@ type CheckPermissionPayload struct {
UID *string `json:"uid"`
Permission *profileutils.Permission `json:"permission"`
}

// CoverLinkingNotificationPayload defines the input for the cover linking process. This happens when
// autolinking fails. Instead of returning an error message, we store the details so that the
// staff can review and either approve or reject the request
type CoverLinkingNotificationPayload struct {
ID string `json:"id" firestore:"id"`
TimeStamp time.Time `json:"timeStamp" firestore:"timeStamp"`
Read bool `json:"read" firestore:"read"`
PayerSladeCode int `json:"payersladecode" firestore:"payerSladeCode"`
MemberNumber string `json:"membernumber" firestore:"memberNumber"`
State string `json:"state" firestore:"state"`
FirstName *string `json:"firstName" firestore:"firstName"`
LastName *string `json:"lastName" firestore:"lastName"`
PhoneNumber string `json:"phoneNumber" firestore:"phoneNumber"`
ErrorMessage string `json:"errorMessage" firestore:"errorMessage"`
}
64 changes: 47 additions & 17 deletions pkg/onboarding/infrastructure/database/fb/firebase.go
Expand Up @@ -35,23 +35,24 @@ var tracer = otel.Tracer(
)

const (
userProfileCollectionName = "user_profiles"
supplierProfileCollectionName = "supplier_profiles"
customerProfileCollectionName = "customer_profiles"
pinsCollectionName = "pins"
surveyCollectionName = "post_visit_survey"
profileNudgesCollectionName = "profile_nudges"
kycProcessCollectionName = "kyc_processing"
experimentParticipantCollectionName = "experiment_participants"
nhifDetailsCollectionName = "nhif_details"
communicationsSettingsCollectionName = "communications_settings"
smsCollectionName = "incoming_sms"
ussdDataCollectioName = "ussd_data"
firebaseExchangeRefreshTokenURL = "https://securetoken.googleapis.com/v1/token?key="
marketingDataCollectionName = "marketing_data"
ussdEventsCollectionName = "ussd_events"
coverLinkingEventsCollectionName = "coverlinking_events"
rolesCollectionName = "user_roles"
userProfileCollectionName = "user_profiles"
supplierProfileCollectionName = "supplier_profiles"
customerProfileCollectionName = "customer_profiles"
pinsCollectionName = "pins"
surveyCollectionName = "post_visit_survey"
profileNudgesCollectionName = "profile_nudges"
kycProcessCollectionName = "kyc_processing"
experimentParticipantCollectionName = "experiment_participants"
nhifDetailsCollectionName = "nhif_details"
communicationsSettingsCollectionName = "communications_settings"
smsCollectionName = "incoming_sms"
ussdDataCollectioName = "ussd_data"
firebaseExchangeRefreshTokenURL = "https://securetoken.googleapis.com/v1/token?key="
marketingDataCollectionName = "marketing_data"
ussdEventsCollectionName = "ussd_events"
coverLinkingEventsCollectionName = "coverlinking_events"
rolesCollectionName = "user_roles"
coverLinkingNotificationCollectionName = "coverlinking_notification"
)

// Repository accesses and updates an item that is stored on Firebase
Expand Down Expand Up @@ -161,6 +162,13 @@ func (fr Repository) GetCoverLinkingEventsCollectionName() string {
return suffixed
}

// GetCoverLinkingNotificationCollectionName returns the collection where cover linking
// notification will be stored
func (fr Repository) GetCoverLinkingNotificationCollectionName() string {
suffixed := firebasetools.SuffixCollection(coverLinkingNotificationCollectionName)
return suffixed
}

// GetRolesCollectionName ...
func (fr Repository) GetRolesCollectionName() string {
suffixed := firebasetools.SuffixCollection(rolesCollectionName)
Expand Down Expand Up @@ -4292,3 +4300,25 @@ func (fr *Repository) CheckIfUserHasPermission(

return false, nil
}

// SaveCoverLinkingNotification saves a cover linking notification to the collection
func (fr *Repository) SaveCoverLinkingNotification(
ctx context.Context,
input *dto.CoverLinkingNotificationPayload,
) error {
ctx, span := tracer.Start(ctx, "SaveCoverLinkingNotification")
defer span.End()

query := &CreateCommand{
CollectionName: fr.GetCoverLinkingNotificationCollectionName(),
Data: input,
}

_, err := fr.FirestoreClient.Create(ctx, query)
if err != nil {
utils.RecordSpanError(span, err)
return exceptions.AddRecordError(err)
}

return nil
}
26 changes: 26 additions & 0 deletions pkg/onboarding/infrastructure/services/edi/mock/service_mock.go
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net/http"

"github.com/savannahghi/onboarding/pkg/onboarding/application/dto"
"gitlab.slade360emr.com/go/apiclient"
)

Expand All @@ -27,6 +28,14 @@ type FakeServiceEDI struct {
membernumber string,
payersladecode int,
) (*http.Response, error)

CreateCoverLinkingRequestFn func(
ctx context.Context,
phoneNumber string,
membernumber string,
payersladecode int,
errorMessage string,
) (*dto.CoverLinkingNotificationPayload, error)
}

// LinkCover ...
Expand Down Expand Up @@ -56,3 +65,20 @@ func (f *FakeServiceEDI) LinkEDIMemberCover(
) (*http.Response, error) {
return f.LinkEDIMemberCoverFn(ctx, phoneNumber, membernumber, payersladecode)
}

// CreateCoverLinkingRequest represents a mock of the CreateCoverLinkingRequest mock
func (f *FakeServiceEDI) CreateCoverLinkingRequest(
ctx context.Context,
phoneNumber string,
membernumber string,
payersladecode int,
errorMessage string,
) (*dto.CoverLinkingNotificationPayload, error) {
return f.CreateCoverLinkingRequestFn(
ctx,
phoneNumber,
membernumber,
payersladecode,
errorMessage,
)
}
91 changes: 89 additions & 2 deletions pkg/onboarding/infrastructure/services/edi/service.go
Expand Up @@ -9,12 +9,14 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/google/uuid"
"github.com/savannahghi/onboarding/pkg/onboarding/application/dto"
"github.com/savannahghi/onboarding/pkg/onboarding/application/extension"
"github.com/savannahghi/onboarding/pkg/onboarding/repository"
"github.com/segmentio/ksuid"
"gitlab.slade360emr.com/go/apiclient"
)

Expand All @@ -27,6 +29,11 @@ const (
CoverLinkingStatusCompleted = "coverlinking completed"

getSladerDataEndpoint = "internal/slader_data?%s"

// PendingState represents the `PENDING` state of a cover linking request
PendingState = "PENDING"
// ApprovedState represents the `APPROVED` state of a cover linking request
ApprovedState = "APPROVED"
)

// ServiceEdi defines the business logic required to interact with EDI
Expand All @@ -46,6 +53,14 @@ type ServiceEdi interface {
membernumber string,
payersladecode int,
) (*http.Response, error)

CreateCoverLinkingRequest(
ctx context.Context,
phoneNumber string,
membernumber string,
payersladecode int,
errorMessage string,
) (*dto.CoverLinkingNotificationPayload, error)
}

// ServiceEDIImpl represents EDI usecases
Expand Down Expand Up @@ -99,7 +114,6 @@ func (e *ServiceEDIImpl) LinkCover(
if err != nil {
return nil, fmt.Errorf("failed to make an edi request for coverlinking: %w", err)
}

currentTime := time.Now()
coverLinkingEvent := &dto.CoverLinkingEvent{
ID: uuid.NewString(),
Expand Down Expand Up @@ -175,10 +189,83 @@ func (e *ServiceEDIImpl) LinkEDIMemberCover(
PushToken: userProfile.PushTokens,
}

return e.EdiExt.MakeRequest(
resp, err := e.EdiExt.MakeRequest(
ctx,
http.MethodPost,
LinkCover,
payload,
)
if err != nil {
return nil, fmt.Errorf("the error is %v", err)
}

if resp.StatusCode != http.StatusOK {
dataResponse, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read request body")
}

data := map[string]interface{}{}
err = json.Unmarshal(dataResponse, &data)
if err != nil {
return nil, fmt.Errorf("bad data returned")
}

// If the response returned has an error, store the details in a collection
// This makes it possible for the staff to review
errorMessage, ok := data["error"]
if ok {
errMessage := errorMessage.(string)
if !strings.Contains(errMessage, "cover already exists") {
_, err := e.CreateCoverLinkingRequest(
ctx,
phoneNumber,
membernumber,
payersladecode,
errMessage,
)
if err != nil {
return nil, err
}
}
}
}

return resp, nil
}

// CreateCoverLinkingRequest creates a cover linking request in the event that
// automatically linking a cover to a user's profile fails
func (e *ServiceEDIImpl) CreateCoverLinkingRequest(
ctx context.Context,
phoneNumber string,
membernumber string,
payersladecode int,
errorMessage string,
) (*dto.CoverLinkingNotificationPayload, error) {
userProfile, err := e.onboardingRepository.GetUserProfileByPhoneNumber(ctx, phoneNumber, false)
if err != nil {
return nil, fmt.Errorf("failed to fetch user profile: %w", err)
}

coverNotificationPayload := &dto.CoverLinkingNotificationPayload{
ID: ksuid.New().String(),
TimeStamp: time.Now(),
Read: false,
PayerSladeCode: payersladecode,
MemberNumber: membernumber,
State: PendingState,
FirstName: userProfile.UserBioData.FirstName,
LastName: userProfile.UserBioData.LastName,
PhoneNumber: phoneNumber,
ErrorMessage: errorMessage,
}

err = e.onboardingRepository.SaveCoverLinkingNotification(ctx, coverNotificationPayload)
if err != nil {
return nil, fmt.Errorf("failed to save cover linking notification: %w", err)
}

// TODO: Send an alert to the ADMIN
return coverNotificationPayload, nil
}
90 changes: 90 additions & 0 deletions pkg/onboarding/infrastructure/services/edi/service_test.go
Expand Up @@ -360,3 +360,93 @@ func TestServiceEDIImpl_LinkEDIMemberCover(t *testing.T) {
})
}
}

func TestServiceEDIImpl_CreateCoverLinkingRequest(t *testing.T) {
e := edi.NewEdiService(ediClient, r)
ctx := context.Background()

firstName := gofakeit.FirstName()
lastName := gofakeit.LastName()

type args struct {
ctx context.Context
phoneNumber string
membernumber string
payersladecode int
errorMessage string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Happy Case -> Create cover linking request",
args: args{
ctx: ctx,
phoneNumber: interserviceclient.TestUserPhoneNumber,
membernumber: "1464441",
payersladecode: 458,
errorMessage: "invalid cover",
},
wantErr: false,
},
{
name: "Sad Case -> Fail to save cover linking request",
args: args{
ctx: ctx,
phoneNumber: interserviceclient.TestUserPhoneNumber,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name == "Happy Case -> Create cover linking request" {
fakeRepo.GetUserProfileByPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) {
return &profileutils.UserProfile{
UserBioData: profileutils.BioData{
FirstName: &firstName,
LastName: &lastName,
},
PushTokens: []string{"Oq70MFGhY7fkoEXiQrRvqMm0BqB3"},
VerifiedUIDS: []string{"Oq70MFGhY7fkoEXiQrRvqMm0BqB3"},
}, nil
}

fakeRepo.SaveCoverLinkingNotificationFn = func(
ctx context.Context,
input *dto.CoverLinkingNotificationPayload,
) error {
return nil
}
}

if tt.name == "Sad Case -> Fail to save cover linking request" {
fakeRepo.GetUserProfileByPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) {
return &profileutils.UserProfile{
UserBioData: profileutils.BioData{
FirstName: &firstName,
LastName: &lastName,
},
PushTokens: []string{"Oq70MFGhY7fkoEXiQrRvqMm0BqB3"},
VerifiedUIDS: []string{"Oq70MFGhY7fkoEXiQrRvqMm0BqB3"},
}, nil
}

fakeRepo.SaveCoverLinkingNotificationFn = func(
ctx context.Context,
input *dto.CoverLinkingNotificationPayload,
) error {
return fmt.Errorf("failed to save cover linking notification")
}
}

_, err := e.CreateCoverLinkingRequest(tt.args.ctx, tt.args.phoneNumber, tt.args.membernumber, tt.args.payersladecode, tt.args.errorMessage)
if (err != nil) != tt.wantErr {
t.Errorf("ServiceEDIImpl.CreateCoverLinkingRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
14 changes: 14 additions & 0 deletions pkg/onboarding/repository/mock/onboarding.go
Expand Up @@ -197,6 +197,12 @@ type FakeOnboardingRepository struct {
CheckIfUserHasPermissionFn func(ctx context.Context, UID string, requiredPermission profileutils.Permission) (bool, error)
UpdateUserProfileEmailFn func(ctx context.Context, phone string, email string) error
GetUserProfilesByRoleIDFn func(ctx context.Context, role string) ([]*profileutils.UserProfile, error)

// covers
SaveCoverLinkingNotificationFn func(
ctx context.Context,
input *dto.CoverLinkingNotificationPayload,
) error
}

// GetSupplierProfileByID ...
Expand Down Expand Up @@ -983,3 +989,11 @@ func (f *FakeOnboardingRepository) GetRoleByName(ctx context.Context, roleName s
func (f *FakeOnboardingRepository) GetUserProfilesByRoleID(ctx context.Context, role string) ([]*profileutils.UserProfile, error) {
return f.GetUserProfilesByRoleIDFn(ctx, role)
}

// SaveCoverLinkingNotification ...
func (f *FakeOnboardingRepository) SaveCoverLinkingNotification(
ctx context.Context,
input *dto.CoverLinkingNotificationPayload,
) error {
return f.SaveCoverLinkingNotificationFn(ctx, input)
}

0 comments on commit 27741e9

Please sign in to comment.