diff --git a/pkg/onboarding/presentation/graph/generated/generated.go b/pkg/onboarding/presentation/graph/generated/generated.go index 3f9b96ec..981eb371 100644 --- a/pkg/onboarding/presentation/graph/generated/generated.go +++ b/pkg/onboarding/presentation/graph/generated/generated.go @@ -294,6 +294,7 @@ type ComplexityRoot struct { RegisterAgent func(childComplexity int, input dto.RegisterAgentInput) int RegisterMicroservice func(childComplexity int, input domain.Microservice) int RegisterPushToken func(childComplexity int, token string) int + ResendTemporaryPin func(childComplexity int, phoneNumber string) int RetireKYCProcessingRequest func(childComplexity int) int RetireSecondaryEmailAddresses func(childComplexity int, emails []string) int RetireSecondaryPhoneNumbers func(childComplexity int, phones []string) int @@ -465,7 +466,7 @@ type ComplexityRoot struct { FetchKYCProcessingRequests func(childComplexity int) int FetchSupplierAllowedLocations func(childComplexity int) int FetchUserNavigationActions func(childComplexity int) int - FindAgentByPhone func(childComplexity int, phoneNumber *string) int + FindAgentbyPhone func(childComplexity int, phoneNumber *string) int FindBranch func(childComplexity int, pagination *firebasetools.PaginationInput, filter []*dto.BranchFilterInput, sort []*dto.BranchSortInput) int FindProvider func(childComplexity int, pagination *firebasetools.PaginationInput, filter []*dto.BusinessPartnerFilterInput, sort []*dto.BusinessPartnerSortInput) int FindUserByPhone func(childComplexity int, phoneNumber string) int @@ -647,6 +648,7 @@ type MutationResolver interface { RevokeRole(ctx context.Context, userID string, roleID string) (bool, error) ActivateRole(ctx context.Context, roleID string) (*dto.RoleOutput, error) DeactivateRole(ctx context.Context, roleID string) (*dto.RoleOutput, error) + ResendTemporaryPin(ctx context.Context, phoneNumber string) (bool, error) } type QueryResolver interface { DummyQuery(ctx context.Context) (*bool, error) @@ -663,7 +665,7 @@ type QueryResolver interface { CheckSupplierKYCSubmitted(ctx context.Context) (bool, error) FetchAdmins(ctx context.Context) ([]*dto.Admin, error) FetchAgents(ctx context.Context) ([]*dto.Agent, error) - FindAgentByPhone(ctx context.Context, phoneNumber *string) (*dto.Agent, error) + FindAgentbyPhone(ctx context.Context, phoneNumber *string) (*dto.Agent, error) FetchUserNavigationActions(ctx context.Context) (*profileutils.NavigationActions, error) ListMicroservices(ctx context.Context) ([]*domain.Microservice, error) GetAllRoles(ctx context.Context, filter *firebasetools.FilterInput) ([]*dto.RoleOutput, error) @@ -2021,6 +2023,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RegisterPushToken(childComplexity, args["token"].(string)), true + case "Mutation.resendTemporaryPin": + if e.complexity.Mutation.ResendTemporaryPin == nil { + break + } + + args, err := ec.field_Mutation_resendTemporaryPin_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ResendTemporaryPin(childComplexity, args["phoneNumber"].(string)), true + case "Mutation.retireKYCProcessingRequest": if e.complexity.Mutation.RetireKYCProcessingRequest == nil { break @@ -2989,7 +3003,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.FetchUserNavigationActions(childComplexity), true case "Query.findAgentbyPhone": - if e.complexity.Query.FindAgentByPhone == nil { + if e.complexity.Query.FindAgentbyPhone == nil { break } @@ -2998,7 +3012,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.FindAgentByPhone(childComplexity, args["phoneNumber"].(*string)), true + return e.complexity.Query.FindAgentbyPhone(childComplexity, args["phoneNumber"].(*string)), true case "Query.findBranch": if e.complexity.Query.FindBranch == nil { @@ -4490,6 +4504,8 @@ extend type Mutation { activateRole(roleID: ID!): RoleOutput! deactivateRole(roleID: ID!): RoleOutput! + + resendTemporaryPin(phoneNumber: String!): Boolean! } `, BuiltIn: false}, {Name: "pkg/onboarding/presentation/graph/types.graphql", Input: `scalar Date @@ -5661,6 +5677,21 @@ func (ec *executionContext) field_Mutation_registerPushToken_args(ctx context.Co return args, nil } +func (ec *executionContext) field_Mutation_resendTemporaryPin_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["phoneNumber"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("phoneNumber")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["phoneNumber"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_retireSecondaryEmailAddresses_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -12734,6 +12765,48 @@ func (ec *executionContext) _Mutation_deactivateRole(ctx context.Context, field return ec.marshalNRoleOutput2ᚖgithubᚗcomᚋsavannahghiᚋonboardingᚋpkgᚋonboardingᚋapplicationᚋdtoᚐRoleOutput(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_resendTemporaryPin(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_resendTemporaryPin_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ResendTemporaryPin(rctx, args["phoneNumber"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _NHIFDetails_id(ctx context.Context, field graphql.CollectedField, obj *domain.NHIFDetails) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -16658,7 +16731,7 @@ func (ec *executionContext) _Query_findAgentbyPhone(ctx context.Context, field g fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().FindAgentByPhone(rctx, args["phoneNumber"].(*string)) + return ec.resolvers.Query().FindAgentbyPhone(rctx, args["phoneNumber"].(*string)) }) if err != nil { ec.Error(ctx, err) @@ -23678,6 +23751,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "resendTemporaryPin": + out.Values[i] = ec._Mutation_resendTemporaryPin(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/onboarding/presentation/graph/profile.graphql b/pkg/onboarding/presentation/graph/profile.graphql index 4ab8519d..940113f7 100644 --- a/pkg/onboarding/presentation/graph/profile.graphql +++ b/pkg/onboarding/presentation/graph/profile.graphql @@ -184,4 +184,6 @@ extend type Mutation { activateRole(roleID: ID!): RoleOutput! deactivateRole(roleID: ID!): RoleOutput! + + resendTemporaryPin(phoneNumber: String!): Boolean! } diff --git a/pkg/onboarding/presentation/graph/profile.resolvers.go b/pkg/onboarding/presentation/graph/profile.resolvers.go index 69e49121..02a399b7 100644 --- a/pkg/onboarding/presentation/graph/profile.resolvers.go +++ b/pkg/onboarding/presentation/graph/profile.resolvers.go @@ -5,6 +5,7 @@ package graph import ( "context" + "fmt" "time" "github.com/savannahghi/enumutils" @@ -681,6 +682,15 @@ func (r *mutationResolver) DeactivateRole(ctx context.Context, roleID string) (* return role, err } +func (r *mutationResolver) ResendTemporaryPin(ctx context.Context, phoneNumber string) (bool, error) { + startTime := time.Now() + + success, err := r.interactor.UserPIN.ResendTemporaryPin(ctx, phoneNumber) + defer serverutils.RecordGraphqlResolverMetrics(ctx, startTime, "resendTemporaryPin", err) + + return success, err +} + func (r *queryResolver) DummyQuery(ctx context.Context) (*bool, error) { dummy := true return &dummy, nil @@ -836,14 +846,8 @@ func (r *queryResolver) FetchAgents(ctx context.Context) ([]*dto.Agent, error) { return agents, err } -func (r *queryResolver) FindAgentByPhone(ctx context.Context, phoneNumber *string) (*dto.Agent, error) { - startTime := time.Now() - - agent, err := r.interactor.Agent.FindAgentByPhone(ctx, phoneNumber) - - defer serverutils.RecordGraphqlResolverMetrics(ctx, startTime, "findAgentbyPhone", err) - - return agent, err +func (r *queryResolver) FindAgentbyPhone(ctx context.Context, phoneNumber *string) (*dto.Agent, error) { + panic(fmt.Errorf("not implemented")) } func (r *queryResolver) FetchUserNavigationActions(ctx context.Context) (*profileutils.NavigationActions, error) { @@ -914,3 +918,19 @@ func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } + +// !!! WARNING !!! +// The code below was going to be deleted when updating resolvers. It has been copied here so you have +// one last chance to move it out of harms way if you want. There are two reasons this happens: +// - When renaming or deleting a resolver the old code will be put in here. You can safely delete +// it when you're done. +// - You have helper methods in this file. Move them out to keep these resolver files clean. +func (r *queryResolver) FindAgentByPhone(ctx context.Context, phoneNumber *string) (*dto.Agent, error) { + startTime := time.Now() + + agent, err := r.interactor.Agent.FindAgentByPhone(ctx, phoneNumber) + + defer serverutils.RecordGraphqlResolverMetrics(ctx, startTime, "findAgentbyPhone", err) + + return agent, err +} diff --git a/pkg/onboarding/usecases/profile.go b/pkg/onboarding/usecases/profile.go index 5473cbc4..fc0863bb 100644 --- a/pkg/onboarding/usecases/profile.go +++ b/pkg/onboarding/usecases/profile.go @@ -182,8 +182,6 @@ type ProfileUseCase interface { SwitchUserFlaggedFeatures(ctx context.Context, phoneNumber string) (*dto.OKResp, error) FindUserByPhone(ctx context.Context, phoneNumber string) (*profileutils.UserProfile, error) - - ResendTemporaryPin(ctx context.Context, phone string) (bool, error) } // ProfileUseCaseImpl represents usecase implementation object @@ -193,7 +191,6 @@ type ProfileUseCaseImpl struct { engagement engagement.ServiceEngagement pubsub pubsubmessaging.ServicePubSub crm crm.ServiceCrm - pin UserPINUseCases } // NewProfileUseCase returns a new a onboarding usecase @@ -203,7 +200,6 @@ func NewProfileUseCase( eng engagement.ServiceEngagement, pubsub pubsubmessaging.ServicePubSub, crm crm.ServiceCrm, - pin UserPINUseCases, ) ProfileUseCase { return &ProfileUseCaseImpl{ onboardingRepository: r, @@ -211,7 +207,6 @@ func NewProfileUseCase( engagement: eng, pubsub: pubsub, crm: crm, - pin: pin, } } @@ -2100,40 +2095,3 @@ func (p *ProfileUseCaseImpl) GetNavigationActions( return navActions, nil } - -//ResendTemporaryPin resends a temporary pin to user during sign up -func (p *ProfileUseCaseImpl) ResendTemporaryPin(ctx context.Context, phone string) (bool, error) { - ctx, span := tracer.Start(ctx, "ResendTemporaryPin") - defer span.End() - phoneNumber, err := p.baseExt.NormalizeMSISDN(phone) - if err != nil { - utils.RecordSpanError(span, err) - return false, exceptions.NormalizeMSISDNError(err) - } - - profile, err := p.onboardingRepository.GetUserProfileByPrimaryPhoneNumber( - ctx, - *phoneNumber, - false, - ) - if err != nil { - utils.RecordSpanError(span, err) - return false, err - } - - otp, err := p.pin.SetUserTempPIN(ctx, profile.ID) - if err != nil { - utils.RecordSpanError(span, err) - return false, err - } - - message := fmt.Sprintf( - "Please use this One Time PIN: %s to log onto Bewell with your phone number. You will be prompted to change the PIN on login. For enquiries call us on 0790360360", - otp, - ) - if err := p.engagement.SendSMS(ctx, []string{*phoneNumber}, message); err != nil { - return false, fmt.Errorf("unable to send agent registration message: %w", err) - } - - return true, nil -} diff --git a/pkg/onboarding/usecases/user_pin.go b/pkg/onboarding/usecases/user_pin.go index 11755800..bf592de6 100644 --- a/pkg/onboarding/usecases/user_pin.go +++ b/pkg/onboarding/usecases/user_pin.go @@ -29,6 +29,7 @@ type UserPINUseCases interface { ChangeUserPIN(ctx context.Context, phone string, pin string) (bool, error) RequestPINReset(ctx context.Context, phone string, appID *string) (*profileutils.OtpResponse, error) CheckHasPIN(ctx context.Context, profileID string) (bool, error) + ResendTemporaryPin(ctx context.Context, phone string) (bool, error) } // UserPinUseCaseImpl represents usecase implementation object @@ -287,3 +288,40 @@ func (u *UserPinUseCaseImpl) SetUserTempPIN(ctx context.Context, profileID strin return pin, nil } + +//ResendTemporaryPin resends a temporary pin to user during sign up +func (u *UserPinUseCaseImpl) ResendTemporaryPin(ctx context.Context, phone string) (bool, error) { + ctx, span := tracer.Start(ctx, "ResendTemporaryPin") + defer span.End() + phoneNumber, err := u.baseExt.NormalizeMSISDN(phone) + if err != nil { + utils.RecordSpanError(span, err) + return false, exceptions.NormalizeMSISDNError(err) + } + + profile, err := u.onboardingRepository.GetUserProfileByPrimaryPhoneNumber( + ctx, + *phoneNumber, + false, + ) + if err != nil { + utils.RecordSpanError(span, err) + return false, err + } + + otp, err := u.SetUserTempPIN(ctx, profile.ID) + if err != nil { + utils.RecordSpanError(span, err) + return false, err + } + + message := fmt.Sprintf( + "Please use this One Time PIN: %s to log onto Bewell with your phone number. You will be prompted to change the PIN on login. For enquiries call us on 0790360360", + otp, + ) + if err := u.engagement.SendSMS(ctx, []string{*phoneNumber}, message); err != nil { + return false, fmt.Errorf("unable to send agent registration message: %w", err) + } + + return true, nil +} diff --git a/pkg/onboarding/usecases/user_pin_test.go b/pkg/onboarding/usecases/user_pin_test.go index 05809b46..dc2ff096 100644 --- a/pkg/onboarding/usecases/user_pin_test.go +++ b/pkg/onboarding/usecases/user_pin_test.go @@ -896,3 +896,165 @@ func TestUserPinUseCaseImpl_SetUserTempPIN(t *testing.T) { }) } } + +func TestUserPinUseCaseImpl_ResendTemporaryPin(t *testing.T) { + ctx := context.Background() + i, err := InitializeFakeOnboardingInteractor() + if err != nil { + t.Errorf("failed to fake initialize onboarding interactor: %v", + err, + ) + return + } + type args struct { + ctx context.Context + phone string + } + + input := args{ctx: ctx, phone: interserviceclient.TestUserPhoneNumber} + + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "sad unable to normalize phone number", + args: input, + want: false, + wantErr: true, + }, + { + name: "sad unable to get userprofile by phone number", + args: input, + want: false, + wantErr: true, + }, + { + name: "sad unable to generate temporary pin", + args: input, + want: false, + wantErr: true, + }, + { + name: "sad unable to save temporary pin", + args: input, + want: false, + wantErr: true, + }, + { + name: "sad unable to send sms", + args: input, + want: false, + wantErr: true, + }, + { + name: "happy sent a new temporary pin", + args: input, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name == "sad unable to normalize phone number" { + fakeBaseExt.NormalizeMSISDNFn = func(msisdn string) (*string, error) { + return nil, fmt.Errorf("unable to normalize phone") + } + } + if tt.name == "sad unable to normalize phone number" { + fakeBaseExt.NormalizeMSISDNFn = func(msisdn string) (*string, error) { + phone := interserviceclient.TestUserPhoneNumber + return &phone, nil + } + fakeRepo.GetUserProfileByPrimaryPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) { + return nil, fmt.Errorf("unable to get user profile by primary phone number") + } + } + if tt.name == "sad unable to generate temporary pin" { + fakeBaseExt.NormalizeMSISDNFn = func(msisdn string) (*string, error) { + phone := interserviceclient.TestUserPhoneNumber + return &phone, nil + } + fakeRepo.GetUserProfileByPrimaryPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) { + return &profileutils.UserProfile{}, nil + } + fakePinExt.GenerateTempPINFn = func(ctx context.Context) (string, error) { + return "", fmt.Errorf("unable to generate temporary pin") + } + } + + if tt.name == "sad unable to save temporary pin" { + fakeBaseExt.NormalizeMSISDNFn = func(msisdn string) (*string, error) { + phone := interserviceclient.TestUserPhoneNumber + return &phone, nil + } + fakeRepo.GetUserProfileByPrimaryPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) { + return &profileutils.UserProfile{}, nil + } + fakePinExt.GenerateTempPINFn = func(ctx context.Context) (string, error) { + return "1234", nil + } + fakePinExt.EncryptPINFn = func(rawPwd string, options *extension.Options) (string, string) { + return "salt", "passw" + } + fakeRepo.SavePINFn = func(ctx context.Context, pin *domain.PIN) (bool, error) { + return false, fmt.Errorf("unable to save pin") + } + } + + if tt.name == "sad unable to send sms" { + fakeBaseExt.NormalizeMSISDNFn = func(msisdn string) (*string, error) { + phone := interserviceclient.TestUserPhoneNumber + return &phone, nil + } + fakeRepo.GetUserProfileByPrimaryPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) { + return &profileutils.UserProfile{}, nil + } + fakePinExt.GenerateTempPINFn = func(ctx context.Context) (string, error) { + return "1234", nil + } + fakePinExt.EncryptPINFn = func(rawPwd string, options *extension.Options) (string, string) { + return "salt", "passw" + } + fakeRepo.SavePINFn = func(ctx context.Context, pin *domain.PIN) (bool, error) { + return true, nil + } + fakeEngagementSvs.SendSMSFn = func(ctx context.Context, phoneNumbers []string, message string) error { + return fmt.Errorf("error unable to send sms notice") + } + } + + if tt.name == "happy sent a new temporary pin" { + fakeBaseExt.NormalizeMSISDNFn = func(msisdn string) (*string, error) { + phone := interserviceclient.TestUserPhoneNumber + return &phone, nil + } + fakeRepo.GetUserProfileByPrimaryPhoneNumberFn = func(ctx context.Context, phoneNumber string, suspended bool) (*profileutils.UserProfile, error) { + return &profileutils.UserProfile{}, nil + } + fakePinExt.GenerateTempPINFn = func(ctx context.Context) (string, error) { + return "1234", nil + } + fakePinExt.EncryptPINFn = func(rawPwd string, options *extension.Options) (string, string) { + return "salt", "passw" + } + fakeRepo.SavePINFn = func(ctx context.Context, pin *domain.PIN) (bool, error) { + return true, nil + } + fakeEngagementSvs.SendSMSFn = func(ctx context.Context, phoneNumbers []string, message string) error { + return nil + } + } + got, err := i.UserPIN.ResendTemporaryPin(tt.args.ctx, tt.args.phone) + if (err != nil) != tt.wantErr { + t.Errorf("UserPinUseCaseImpl.ResendTemporaryPin() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("UserPinUseCaseImpl.ResendTemporaryPin() = %v, want %v", got, tt.want) + } + }) + } +}