diff --git a/go.mod b/go.mod index c1b18a5a..d0c615e9 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,7 @@ require ( firebase.google.com/go v3.13.0+incompatible github.com/99designs/gqlgen v0.13.0 github.com/GoogleCloudPlatform/cloudsql-proxy v1.26.0 - github.com/Pallinder/go-randomdata v1.2.0 // indirect - github.com/brianvoe/gofakeit v3.18.0+incompatible + github.com/Pallinder/go-randomdata v1.2.0 github.com/casbin/casbin/v2 v2.31.3 github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 diff --git a/pkg/onboarding/application/enums/identifier.go b/pkg/onboarding/application/enums/identifier.go new file mode 100644 index 00000000..3d96bca0 --- /dev/null +++ b/pkg/onboarding/application/enums/identifier.go @@ -0,0 +1,115 @@ +package enums + +import ( + "fmt" + "io" + "strconv" +) + +// IdentifierType defines the various identifier types +type IdentifierType string + +const ( + // IdentifierTypeCCC represents a Comprehensive Care Centre identifier type + IdentifierTypeCCC IdentifierType = "CCC" + + // IdentifierTypeID represents the national ID identifier type + IdentifierTypeID IdentifierType = "NATIONAL ID" + + // IdentifierTypePassport represents a passport identifier type + IdentifierTypePassport IdentifierType = "PASSPORT" +) + +// AllIdentifierType represents a slice of all possible `IdentifierTypes` values +var AllIdentifierType = []IdentifierType{ + IdentifierTypeCCC, + IdentifierTypeID, + IdentifierTypePassport, +} + +// IsValid returns true if an identifier type is valid +func (e IdentifierType) IsValid() bool { + switch e { + case IdentifierTypeCCC, IdentifierTypeID, IdentifierTypePassport: + return true + } + return false +} + +// String ... +func (e IdentifierType) String() string { + return string(e) +} + +// UnmarshalGQL converts the supplied value to a metric type. +func (e *IdentifierType) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = IdentifierType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid IdentifierType", str) + } + return nil +} + +// MarshalGQL writes the metric type to the supplied writer +func (e IdentifierType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +// IdentifierUse defines the different kinds of identifiers use +type IdentifierUse string + +const ( + // IdentifierUseOfficial represents an `official` identifier use + IdentifierUseOfficial IdentifierUse = "OFFICIAL" + + // IdentifierUseTemporary represents a `temporary` identifier use + IdentifierUseTemporary IdentifierUse = "TEMPORARY" + + // IdentifierUseOld represents an `old` identifier use + IdentifierUseOld IdentifierUse = "OLD" +) + +// AllIdentifierUse represents a slice of all possible `IdentifierUse` values +var AllIdentifierUse = []IdentifierUse{ + IdentifierUseOfficial, + IdentifierUseTemporary, + IdentifierUseOld, +} + +// IsValid returns true if an identifier use is valid +func (e IdentifierUse) IsValid() bool { + switch e { + case IdentifierUseOfficial, IdentifierUseTemporary, IdentifierUseOld: + return true + } + return false +} + +// String ... +func (e IdentifierUse) String() string { + return string(e) +} + +// UnmarshalGQL converts the supplied value to a metric type. +func (e *IdentifierUse) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = IdentifierUse(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid IdentifierUse", str) + } + return nil +} + +// MarshalGQL writes the metric type to the supplied writer +func (e IdentifierUse) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} diff --git a/pkg/onboarding/domain/models.go b/pkg/onboarding/domain/models.go index e71fe8da..d935f933 100644 --- a/pkg/onboarding/domain/models.go +++ b/pkg/onboarding/domain/models.go @@ -111,19 +111,19 @@ type UserPIN struct { // Identifier are specific/unique identifiers for a user type Identifier struct { - ID *string // globally unique identifier - ClientID string // TODO: FK to client - IdentifierType string // TODO: Enum; start with basics e.g CCC number, ID number - IdentifierUse string // TODO: Enum; e.g official, temporary, old (see FHIR Person for enum) + ID *string `json:"id"` + ClientID string `json:"clientID"` + IdentifierType enums.IdentifierType `json:"identifierType"` + IdentifierUse enums.IdentifierUse `json:"identifierUse"` // TODO: Validate identifier value against type e.g format of CCC number // TODO: Unique together: identifier value & type i.e the same identifier can't be used for more than one client - IdentifierValue string // the actual identifier e.g CCC number - Description string - ValidFrom *time.Time - ValidTo *time.Time - Active bool - IsPrimaryIdentifier bool + IdentifierValue string `json:"identifierValue"` + Description string `json:"description"` + ValidFrom *time.Time `json:"validFrom"` + ValidTo *time.Time `json:"validTo"` + Active bool `json:"active"` + IsPrimaryIdentifier bool `json:"isPrimaryIdentifier"` } // ClientProfile holds the details of end users who are not using the system in @@ -138,7 +138,7 @@ type ClientProfile struct { // the client record is for bridging to other identifiers e.g patient record IDs UserID *string // TODO: Foreign key to User - TreatmentEnrollmentDate *scalarutils.Date // use for date of treatment enrollment + TreatmentEnrollmentDate *time.Time // use for date of treatment enrollment ClientType enums.ClientType diff --git a/pkg/onboarding/infrastructure/database/postgres/gorm/create.go b/pkg/onboarding/infrastructure/database/postgres/gorm/create.go index c5024f35..b88b12d9 100644 --- a/pkg/onboarding/infrastructure/database/postgres/gorm/create.go +++ b/pkg/onboarding/infrastructure/database/postgres/gorm/create.go @@ -16,6 +16,11 @@ type Create interface { userInput *User, clientInput *ClientProfile, ) (*ClientUserProfile, error) + + AddIdentifier( + ctx context.Context, + identifier *Identifier, + ) (*Identifier, error) } // GetOrCreateFacility ... @@ -132,3 +137,11 @@ func (db *PGInstance) RegisterClient( }, nil } + +// AddIdentifier saves a client's identifier record to the database +func (db *PGInstance) AddIdentifier(ctx context.Context, identifier *Identifier) (*Identifier, error) { + if err := db.DB.Create(identifier).Error; err != nil { + return nil, fmt.Errorf("failed to create identifier: %v", err) + } + return identifier, nil +} diff --git a/pkg/onboarding/infrastructure/database/postgres/gorm/mock/gorm_mock.go b/pkg/onboarding/infrastructure/database/postgres/gorm/mock/gorm_mock.go index fe81c770..4897e805 100644 --- a/pkg/onboarding/infrastructure/database/postgres/gorm/mock/gorm_mock.go +++ b/pkg/onboarding/infrastructure/database/postgres/gorm/mock/gorm_mock.go @@ -17,17 +17,19 @@ import ( // // This mock struct should be separate from our own internal methods. type GormMock struct { - GetOrCreateFacilityFn func(ctx context.Context, facility *gorm.Facility) (*gorm.Facility, error) - RetrieveFacilityFn func(ctx context.Context, id *string, isActive bool) (*gorm.Facility, error) - RetrieveFacilityByMFLCodeFn func(ctx context.Context, MFLCode string, isActive bool) (*gorm.Facility, error) - GetFacilitiesFn func(ctx context.Context) ([]gorm.Facility, error) - DeleteFacilityFn func(ctx context.Context, mfl_code string) (bool, error) - CollectMetricsFn func(ctx context.Context, metrics *gorm.Metric) (*gorm.Metric, error) - SetUserPINFn func(ctx context.Context, pinData *gorm.PINData) (bool, error) - GetUserPINByUserIDFn func(ctx context.Context, userID string) (*gorm.PINData, error) - GetUserProfileByUserIDFn func(ctx context.Context, userID string, flavour string) (*gorm.User, error) - RegisterStaffUserFn func(ctx context.Context, user *gorm.User, staff *gorm.StaffProfile) (*gorm.StaffUserProfile, error) - RegisterClientFn func(ctx context.Context, userInput *gorm.User, clientInput *gorm.ClientProfile) (*gorm.ClientUserProfile, error) + GetOrCreateFacilityFn func(ctx context.Context, facility *gorm.Facility) (*gorm.Facility, error) + RetrieveFacilityFn func(ctx context.Context, id *string, isActive bool) (*gorm.Facility, error) + RetrieveFacilityByMFLCodeFn func(ctx context.Context, MFLCode string, isActive bool) (*gorm.Facility, error) + GetFacilitiesFn func(ctx context.Context) ([]gorm.Facility, error) + DeleteFacilityFn func(ctx context.Context, mfl_code string) (bool, error) + CollectMetricsFn func(ctx context.Context, metrics *gorm.Metric) (*gorm.Metric, error) + SetUserPINFn func(ctx context.Context, pinData *gorm.PINData) (bool, error) + GetUserPINByUserIDFn func(ctx context.Context, userID string) (*gorm.PINData, error) + GetUserProfileByUserIDFn func(ctx context.Context, userID string, flavour string) (*gorm.User, error) + RegisterStaffUserFn func(ctx context.Context, user *gorm.User, staff *gorm.StaffProfile) (*gorm.StaffUserProfile, error) + RegisterClientFn func(ctx context.Context, userInput *gorm.User, clientInput *gorm.ClientProfile) (*gorm.ClientUserProfile, error) + AddIdentifierFn func(ctx context.Context, identifier *gorm.Identifier) (*gorm.Identifier, error) + GetClientProfileByClientIDFn func(ctx context.Context, clientID string) (*gorm.ClientProfile, error) //Updates UpdateUserLastSuccessfulLoginFn func(ctx context.Context, userID string, lastLoginTime time.Time, flavour string) error @@ -55,6 +57,24 @@ func NewGormMock() *GormMock { }, nil }, + AddIdentifierFn: func(ctx context.Context, identifier *gorm.Identifier) (*gorm.Identifier, error) { + return &gorm.Identifier{ + ClientID: identifier.ClientID, + IdentifierType: enums.IdentifierTypeCCC, + IdentifierUse: enums.IdentifierUseOfficial, + IdentifierValue: "Just a random value", + Description: "Random description", + }, nil + }, + + GetClientProfileByClientIDFn: func(ctx context.Context, clientID string) (*gorm.ClientProfile, error) { + ID := uuid.New().String() + return &gorm.ClientProfile{ + ID: &clientID, + UserID: &ID, + }, nil + }, + GetOrCreateFacilityFn: func(ctx context.Context, facility *gorm.Facility) (*gorm.Facility, error) { id := uuid.New().String() name := "Kanairo One" @@ -309,3 +329,16 @@ func (gm *GormMock) RegisterClient( ) (*gorm.ClientUserProfile, error) { return gm.RegisterClientFn(ctx, userInput, clientInput) } + +// AddIdentifier mocks the `AddIdentifier` implementation +func (gm *GormMock) AddIdentifier( + ctx context.Context, + identifier *gorm.Identifier, +) (*gorm.Identifier, error) { + return gm.AddIdentifierFn(ctx, identifier) +} + +// GetClientProfileByClientID mocks the method that fetches a client profile by the ID +func (gm *GormMock) GetClientProfileByClientID(ctx context.Context, clientID string) (*gorm.ClientProfile, error) { + return gm.GetClientProfileByClientIDFn(ctx, clientID) +} diff --git a/pkg/onboarding/infrastructure/database/postgres/gorm/query.go b/pkg/onboarding/infrastructure/database/postgres/gorm/query.go index 00341d50..634aecab 100644 --- a/pkg/onboarding/infrastructure/database/postgres/gorm/query.go +++ b/pkg/onboarding/infrastructure/database/postgres/gorm/query.go @@ -16,6 +16,7 @@ type Query interface { GetFacilities(ctx context.Context) ([]Facility, error) GetUserProfileByUserID(ctx context.Context, userID string, flavour string) (*User, error) GetUserPINByUserID(ctx context.Context, userID string) (*PINData, error) + GetClientProfileByClientID(ctx context.Context, clientID string) (*ClientProfile, error) } // RetrieveFacility fetches a single facility @@ -68,3 +69,13 @@ func (db *PGInstance) GetFacilities(ctx context.Context) ([]Facility, error) { log.Printf("these are the facilities %v", facility) return facility, nil } + +// GetClientProfileByClientID retrieves a client profile by ID +func (db *PGInstance) GetClientProfileByClientID(ctx context.Context, clientID string) (*ClientProfile, error) { + var client ClientProfile + if err := db.DB.Where(&ClientProfile{ID: &clientID}).First(&client).Error; err != nil { + return nil, err + } + + return &client, nil +} diff --git a/pkg/onboarding/infrastructure/database/postgres/gorm/tables.go b/pkg/onboarding/infrastructure/database/postgres/gorm/tables.go index 6b741e5a..c50b8c6e 100644 --- a/pkg/onboarding/infrastructure/database/postgres/gorm/tables.go +++ b/pkg/onboarding/infrastructure/database/postgres/gorm/tables.go @@ -257,7 +257,7 @@ type ClientProfile struct { // TODO: a client can have many identifiers; an identifier belongs to a client // (implement reverse relation lookup) - // Identifiers []*domain.Identifier `gorm:"column:identifiers"` + Identifiers []*Identifier `gorm:"foreignKey:ClientID"` // Addresses []*domain.UserAddress `gorm:"column:addresses"` @@ -318,6 +318,39 @@ func (PINData) TableName() string { return "pindata" } +// Identifier are specific/unique identifiers for a user +type Identifier struct { + Base + + ID *string `gorm:"primaryKey;unique;column:id"` + + ClientID string `gorm:"column:client_id"` + + IdentifierType enums.IdentifierType `gorm:"identifier_type"` + IdentifierUse enums.IdentifierUse `gorm:"identifier_use"` + + // TODO: Validate identifier value against type e.g format of CCC number + // TODO: Unique together: identifier value & type i.e the same identifier can't be used for more than one client + IdentifierValue string `gorm:"identifier_value"` + Description string `gorm:"description"` + ValidFrom *time.Time `gorm:"valid_from"` + ValidTo *time.Time `gorm:"valid_to"` + Active bool `gorm:"active"` + IsPrimaryIdentifier bool `gorm:"is_primary_identifier"` +} + +// BeforeCreate is a hook run before creating a client profile +func (c *Identifier) BeforeCreate(tx *gorm.DB) (err error) { + id := uuid.New().String() + c.ID = &id + return +} + +// TableName customizes how the table name is generated +func (Identifier) TableName() string { + return "client_clientidentifier" +} + func allTables() []interface{} { tables := []interface{}{ &Facility{}, @@ -326,6 +359,7 @@ func allTables() []interface{} { &Contact{}, &StaffProfile{}, &ClientProfile{}, + &Identifier{}, &UserAddress{}, &PINData{}, } diff --git a/pkg/onboarding/infrastructure/database/postgres/mappers.go b/pkg/onboarding/infrastructure/database/postgres/mappers.go index aebe70f7..f67a6dce 100644 --- a/pkg/onboarding/infrastructure/database/postgres/mappers.go +++ b/pkg/onboarding/infrastructure/database/postgres/mappers.go @@ -180,3 +180,44 @@ func createMapUser(userObject *gorm.User) *domain.User { } return user } + +// mapIdentifierObjectToDomain maps the identifier object to our domain defined type +func (d *OnboardingDb) mapIdentifierObjectToDomain(identifierObject *gorm.Identifier) *domain.Identifier { + if identifierObject == nil { + return nil + } + + return &domain.Identifier{ + ID: identifierObject.ID, + ClientID: identifierObject.ClientID, + IdentifierType: identifierObject.IdentifierType, + IdentifierUse: identifierObject.IdentifierUse, + IdentifierValue: identifierObject.IdentifierValue, + Description: identifierObject.Description, + ValidFrom: identifierObject.ValidFrom, + ValidTo: identifierObject.ValidTo, + Active: identifierObject.Active, + IsPrimaryIdentifier: identifierObject.IsPrimaryIdentifier, + } +} + +// mapClientObjectToDomain maps the client object to the domain defined type +func (d *OnboardingDb) mapClientObjectToDomain(client *gorm.ClientProfile) *domain.ClientProfile { + if client == nil { + return nil + } + + return &domain.ClientProfile{ + ID: client.ID, + UserID: client.UserID, + TreatmentEnrollmentDate: client.TreatmentEnrollmentDate, + ClientType: client.ClientType, + Active: client.Active, + HealthRecordID: client.HealthRecordID, + // Identifiers: client.Identifiers, + FacilityID: client.FacilityID, + TreatmentBuddyUserID: client.TreatmentBuddy, + CHVUserID: client.CHVUserID, + ClientCounselled: client.ClientCounselled, + } +} diff --git a/pkg/onboarding/infrastructure/database/postgres/pg_create.go b/pkg/onboarding/infrastructure/database/postgres/pg_create.go index 92bb4e71..6a32c0f0 100644 --- a/pkg/onboarding/infrastructure/database/postgres/pg_create.go +++ b/pkg/onboarding/infrastructure/database/postgres/pg_create.go @@ -8,6 +8,7 @@ import ( "github.com/lib/pq" "github.com/savannahghi/onboarding-service/pkg/onboarding/application/dto" + "github.com/savannahghi/onboarding-service/pkg/onboarding/application/enums" "github.com/savannahghi/onboarding-service/pkg/onboarding/domain" "github.com/savannahghi/onboarding-service/pkg/onboarding/infrastructure/database/postgres/gorm" ) @@ -108,6 +109,31 @@ func (d *OnboardingDb) RegisterStaffUser(ctx context.Context, user *dto.UserInpu } +// AddIdentifier is responsible for creating an identifier and associating it with a specific client +func (d *OnboardingDb) AddIdentifier( + ctx context.Context, + clientID string, + idType enums.IdentifierType, + idValue string, + isPrimary bool, +) (*domain.Identifier, error) { + identifierPayload := &gorm.Identifier{ + ClientID: clientID, + IdentifierType: idType, + IdentifierValue: idValue, + IdentifierUse: enums.IdentifierUseOfficial, + IsPrimaryIdentifier: isPrimary, + Active: true, + } + + identifier, err := d.create.AddIdentifier(ctx, identifierPayload) + if err != nil { + return nil, fmt.Errorf("failed to create identifier: %v", err) + } + + return d.mapIdentifierObjectToDomain(identifier), nil +} + // RegisterClient is responsible for registering and saving the client's data to the database func (d *OnboardingDb) RegisterClient( ctx context.Context, diff --git a/pkg/onboarding/infrastructure/database/postgres/pg_create_test.go b/pkg/onboarding/infrastructure/database/postgres/pg_create_test.go index b473b2cd..9403802f 100644 --- a/pkg/onboarding/infrastructure/database/postgres/pg_create_test.go +++ b/pkg/onboarding/infrastructure/database/postgres/pg_create_test.go @@ -439,3 +439,63 @@ func TestOnboardingDb_RegisterClient(t *testing.T) { }) } } + +func TestOnboardingDb_AddIdentifier(t *testing.T) { + ctx := context.Background() + type args struct { + ctx context.Context + clientID string + idType enums.IdentifierType + idValue string + isPrimary bool + } + tests := []struct { + name string + args args + want *domain.Identifier + wantErr bool + }{ + { + name: "Happy Case - Successfully add identifier", + args: args{ + ctx: ctx, + clientID: "12345", + idType: enums.IdentifierTypeCCC, + idValue: "1224", + isPrimary: true, + }, + wantErr: false, + }, + { + name: "Sad Case - Fail to add identifier", + args: args{ + ctx: ctx, + clientID: "12345", + idType: enums.IdentifierTypeCCC, + idValue: "1224", + isPrimary: true, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var fakeGorm = gormMock.NewGormMock() + d := NewOnboardingDb(fakeGorm, fakeGorm, fakeGorm, fakeGorm) + + if tt.name == "Sad Case - Fail to add identifier" { + fakeGorm.AddIdentifierFn = func(ctx context.Context, identifier *gorm.Identifier) (*gorm.Identifier, error) { + return nil, fmt.Errorf("failed to add identifier") + } + } + got, err := d.AddIdentifier(tt.args.ctx, tt.args.clientID, tt.args.idType, tt.args.idValue, tt.args.isPrimary) + if (err != nil) != tt.wantErr { + t.Errorf("OnboardingDb.AddIdentifier() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got == nil { + t.Errorf("expected a response but got :%v", got) + } + }) + } +} diff --git a/pkg/onboarding/infrastructure/database/postgres/pg_query.go b/pkg/onboarding/infrastructure/database/postgres/pg_query.go index e7556367..de72d5fc 100644 --- a/pkg/onboarding/infrastructure/database/postgres/pg_query.go +++ b/pkg/onboarding/infrastructure/database/postgres/pg_query.go @@ -84,3 +84,13 @@ func (d *OnboardingDb) GetUserPINByUserID(ctx context.Context, userID string) (* return d.mapPINObjectToDomain(pinData), nil } + +// GetClientProfileByClientID retrieves a client profile using the client ID +func (d *OnboardingDb) GetClientProfileByClientID(ctx context.Context, clientID string) (*domain.ClientProfile, error) { + client, err := d.query.GetClientProfileByClientID(ctx, clientID) + if err != nil { + return nil, err + } + + return d.mapClientObjectToDomain(client), err +} diff --git a/pkg/onboarding/infrastructure/database/postgres/pg_query_test.go b/pkg/onboarding/infrastructure/database/postgres/pg_query_test.go index a71accc9..e5f7bcf2 100644 --- a/pkg/onboarding/infrastructure/database/postgres/pg_query_test.go +++ b/pkg/onboarding/infrastructure/database/postgres/pg_query_test.go @@ -299,3 +299,54 @@ func TestOnboardingDb_RetrieveByFacilityMFLCode(t *testing.T) { }) } } + +func TestOnboardingDb_GetClientProfileByClientID(t *testing.T) { + ctx := context.Background() + var fakeGorm = gormMock.NewGormMock() + d := NewOnboardingDb(fakeGorm, fakeGorm, fakeGorm, fakeGorm) + + type args struct { + ctx context.Context + clientID string + } + tests := []struct { + name string + args args + want *domain.ClientProfile + wantErr bool + }{ + { + name: "Happy Case - Successfully fetch client profile", + args: args{ + ctx: ctx, + clientID: "1234", + }, + wantErr: false, + }, + { + name: "Sad Case - Fail to get profile", + args: args{ + ctx: ctx, + clientID: "1234", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name == "Sad Case - Fail to get profile" { + fakeGorm.GetClientProfileByClientIDFn = func(ctx context.Context, clientID string) (*gorm.ClientProfile, error) { + return nil, fmt.Errorf("failed to get client profile by ID") + } + } + got, err := d.GetClientProfileByClientID(tt.args.ctx, tt.args.clientID) + if (err != nil) != tt.wantErr { + t.Errorf("OnboardingDb.GetClientProfileByClientID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got == nil { + t.Errorf("expected a response but got :%v", got) + } + }) + } +} diff --git a/pkg/onboarding/infrastructure/repository.go b/pkg/onboarding/infrastructure/repository.go index 5c6fada3..83148782 100644 --- a/pkg/onboarding/infrastructure/repository.go +++ b/pkg/onboarding/infrastructure/repository.go @@ -5,6 +5,7 @@ import ( "time" "github.com/savannahghi/onboarding-service/pkg/onboarding/application/dto" + "github.com/savannahghi/onboarding-service/pkg/onboarding/application/enums" "github.com/savannahghi/onboarding-service/pkg/onboarding/domain" pg "github.com/savannahghi/onboarding-service/pkg/onboarding/infrastructure/database/postgres" ) @@ -22,6 +23,13 @@ type Create interface { userInput *dto.UserInput, clientInput *dto.ClientProfileInput, ) (*domain.ClientUserProfile, error) + AddIdentifier( + ctx context.Context, + clientID string, + idType enums.IdentifierType, + idValue string, + isPrimary bool, + ) (*domain.Identifier, error) } // Delete represents all the deletion action interfaces @@ -61,6 +69,17 @@ func (f ServiceCreateImpl) RegisterStaffUser(ctx context.Context, user *dto.User return f.onboarding.RegisterStaffUser(ctx, user, staff) } +// AddIdentifier adds an identifier that is associated to a given client +func (f ServiceCreateImpl) AddIdentifier( + ctx context.Context, + clientID string, + idType enums.IdentifierType, + idValue string, + isPrimary bool, +) (*domain.Identifier, error) { + return f.onboarding.AddIdentifier(ctx, clientID, idType, idValue, isPrimary) +} + // RegisterClient creates a client user and saves the details in the database func (f ServiceCreateImpl) RegisterClient( ctx context.Context, @@ -77,6 +96,7 @@ type Query interface { RetrieveFacilityByMFLCode(ctx context.Context, MFLCode string, isActive bool) (*domain.Facility, error) GetUserProfileByUserID(ctx context.Context, userID string, flavour string) (*domain.User, error) GetUserPINByUserID(ctx context.Context, userID string) (*domain.UserPIN, error) + GetClientProfileByClientID(ctx context.Context, clientID string) (*domain.ClientProfile, error) } // ServiceQueryImpl contains implementation for the Query interface @@ -121,6 +141,11 @@ func (q ServiceQueryImpl) GetUserPINByUserID(ctx context.Context, userID string) return q.onboarding.GetUserPINByUserID(ctx, userID) } +// GetClientProfileByClientID fetches a client profile using the client ID +func (q ServiceQueryImpl) GetClientProfileByClientID(ctx context.Context, clientID string) (*domain.ClientProfile, error) { + return q.onboarding.GetClientProfileByClientID(ctx, clientID) +} + // ServiceDeleteImpl represents delete facility implementation object type ServiceDeleteImpl struct { onboarding pg.OnboardingDb diff --git a/pkg/onboarding/presentation/graph/client.graphql b/pkg/onboarding/presentation/graph/client.graphql index 82683e9d..9e46e319 100644 --- a/pkg/onboarding/presentation/graph/client.graphql +++ b/pkg/onboarding/presentation/graph/client.graphql @@ -3,4 +3,11 @@ extend type Mutation { userInput: UserInput! clientInput: ClientProfileInput! ): ClientUserProfile! + + addIdentifier( + clientID: String! + idType: IdentifierType! + idValue: String! + isPrimary: Boolean! + ): Identifier! } diff --git a/pkg/onboarding/presentation/graph/client.resolvers.go b/pkg/onboarding/presentation/graph/client.resolvers.go index a18434d2..955ee10c 100644 --- a/pkg/onboarding/presentation/graph/client.resolvers.go +++ b/pkg/onboarding/presentation/graph/client.resolvers.go @@ -7,6 +7,7 @@ import ( "context" "github.com/savannahghi/onboarding-service/pkg/onboarding/application/dto" + "github.com/savannahghi/onboarding-service/pkg/onboarding/application/enums" "github.com/savannahghi/onboarding-service/pkg/onboarding/domain" "github.com/savannahghi/onboarding-service/pkg/onboarding/presentation/graph/generated" ) @@ -15,6 +16,10 @@ func (r *mutationResolver) RegisterClientUser(ctx context.Context, userInput dto return r.interactor.ClientUseCase.RegisterClient(ctx, &userInput, &clientInput) } +func (r *mutationResolver) AddIdentifier(ctx context.Context, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) (*domain.Identifier, error) { + return r.interactor.ClientUseCase.AddIdentifier(ctx, clientID, idType, idValue, isPrimary) +} + // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } diff --git a/pkg/onboarding/presentation/graph/enums.graphql b/pkg/onboarding/presentation/graph/enums.graphql index cdbf2325..f2867186 100644 --- a/pkg/onboarding/presentation/graph/enums.graphql +++ b/pkg/onboarding/presentation/graph/enums.graphql @@ -2,3 +2,15 @@ enum ClientType { PMTCT OVC } + +enum IdentifierType { + CCC + ID + PASSPORT +} + +enum IdentifierUse { + OFFICIAL + TEMPORARY + OLD +} diff --git a/pkg/onboarding/presentation/graph/generated/generated.go b/pkg/onboarding/presentation/graph/generated/generated.go index dbf3a549..8a67a538 100644 --- a/pkg/onboarding/presentation/graph/generated/generated.go +++ b/pkg/onboarding/presentation/graph/generated/generated.go @@ -127,6 +127,19 @@ type ComplexityRoot struct { Secondary func(childComplexity int) int } + Identifier struct { + Active func(childComplexity int) int + ClientID func(childComplexity int) int + Description func(childComplexity int) int + ID func(childComplexity int) int + IdentifierType func(childComplexity int) int + IdentifierUse func(childComplexity int) int + IdentifierValue func(childComplexity int) int + IsPrimaryIdentifier func(childComplexity int) int + ValidFrom func(childComplexity int) int + ValidTo func(childComplexity int) int + } + Link struct { Description func(childComplexity int) int ID func(childComplexity int) int @@ -146,6 +159,7 @@ type ComplexityRoot struct { Mutation struct { ActivateRole func(childComplexity int, roleID string) int AddAddress func(childComplexity int, input dto.UserAddressInput, addressType enumutils.AddressType) int + AddIdentifier func(childComplexity int, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) int AddPermissionsToRole func(childComplexity int, input dto.RolePermissionInput) int AddSecondaryEmailAddress func(childComplexity int, email []string) int AddSecondaryPhoneNumber func(childComplexity int, phone []string) int @@ -351,6 +365,7 @@ type EntityResolver interface { } type MutationResolver interface { RegisterClientUser(ctx context.Context, userInput dto1.UserInput, clientInput dto1.ClientProfileInput) (*domain1.ClientUserProfile, error) + AddIdentifier(ctx context.Context, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) (*domain1.Identifier, error) CreateFacility(ctx context.Context, input dto1.FacilityInput) (*domain1.Facility, error) DeleteFacility(ctx context.Context, id string) (bool, error) SetUserPin(ctx context.Context, input *dto1.PINInput) (bool, error) @@ -731,6 +746,76 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.GroupedNavigationActions.Secondary(childComplexity), true + case "Identifier.active": + if e.complexity.Identifier.Active == nil { + break + } + + return e.complexity.Identifier.Active(childComplexity), true + + case "Identifier.clientID": + if e.complexity.Identifier.ClientID == nil { + break + } + + return e.complexity.Identifier.ClientID(childComplexity), true + + case "Identifier.description": + if e.complexity.Identifier.Description == nil { + break + } + + return e.complexity.Identifier.Description(childComplexity), true + + case "Identifier.id": + if e.complexity.Identifier.ID == nil { + break + } + + return e.complexity.Identifier.ID(childComplexity), true + + case "Identifier.identifierType": + if e.complexity.Identifier.IdentifierType == nil { + break + } + + return e.complexity.Identifier.IdentifierType(childComplexity), true + + case "Identifier.identifierUse": + if e.complexity.Identifier.IdentifierUse == nil { + break + } + + return e.complexity.Identifier.IdentifierUse(childComplexity), true + + case "Identifier.identifierValue": + if e.complexity.Identifier.IdentifierValue == nil { + break + } + + return e.complexity.Identifier.IdentifierValue(childComplexity), true + + case "Identifier.isPrimaryIdentifier": + if e.complexity.Identifier.IsPrimaryIdentifier == nil { + break + } + + return e.complexity.Identifier.IsPrimaryIdentifier(childComplexity), true + + case "Identifier.validFrom": + if e.complexity.Identifier.ValidFrom == nil { + break + } + + return e.complexity.Identifier.ValidFrom(childComplexity), true + + case "Identifier.validTo": + if e.complexity.Identifier.ValidTo == nil { + break + } + + return e.complexity.Identifier.ValidTo(childComplexity), true + case "Link.Description": if e.complexity.Link.Description == nil { break @@ -825,6 +910,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AddAddress(childComplexity, args["input"].(dto.UserAddressInput), args["addressType"].(enumutils.AddressType)), true + case "Mutation.addIdentifier": + if e.complexity.Mutation.AddIdentifier == nil { + break + } + + args, err := ec.field_Mutation_addIdentifier_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AddIdentifier(childComplexity, args["clientID"].(string), args["idType"].(enums.IdentifierType), args["idValue"].(string), args["isPrimary"].(bool)), true + case "Mutation.addPermissionsToRole": if e.complexity.Mutation.AddPermissionsToRole == nil { break @@ -2083,6 +2180,13 @@ var sources = []*ast.Source{ userInput: UserInput! clientInput: ClientProfileInput! ): ClientUserProfile! + + addIdentifier( + clientID: String! + idType: IdentifierType! + idValue: String! + isPrimary: Boolean! + ): Identifier! } `, BuiltIn: false}, {Name: "pkg/onboarding/presentation/graph/emum.graphql", Input: `enum ContactType { @@ -2099,6 +2203,17 @@ enum UsersType { PMTCT OVC } + +enum IdentifierType { + CCC + ID + PASSPORT +} + +enum IdentifierUse { + OFFICIAL + TEMPORARY +} `, BuiltIn: false}, {Name: "pkg/onboarding/presentation/graph/facility.graphql", Input: ` extend type Mutation { @@ -2182,7 +2297,6 @@ extend type Mutation { description: String! } - type PIN { user: String! pin: String! @@ -2214,12 +2328,12 @@ type User { type Contact { ID: String! - Type: ContactType! - Contact: String! #TODO Validate: phones are E164, emails are valid - Active: Boolean! + Type: ContactType! + Contact: String! #TODO Validate: phones are E164, emails are valid + Active: Boolean! #a user may opt not to be contacted via this contact #e.g if it's a shared phone owned by a teenager - OptedIn: Boolean! + OptedIn: Boolean! } type StaffProfile { @@ -2254,6 +2368,19 @@ type ClientUserProfile { user: User! client: ClientProfile! } + +type Identifier { + id: String! + clientID: String! + identifierType: IdentifierType! + identifierUse: IdentifierUse! + identifierValue: String! + description: String + validFrom: Date + validTo: Date + active: Boolean! + isPrimaryIdentifier: Boolean! +} `, BuiltIn: false}, {Name: "federation/directives.graphql", Input: ` scalar _Any @@ -2819,6 +2946,48 @@ func (ec *executionContext) field_Mutation_addAddress_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_addIdentifier_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["clientID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("clientID")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["clientID"] = arg0 + var arg1 enums.IdentifierType + if tmp, ok := rawArgs["idType"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idType")) + arg1, err = ec.unmarshalNIdentifierType2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierType(ctx, tmp) + if err != nil { + return nil, err + } + } + args["idType"] = arg1 + var arg2 string + if tmp, ok := rawArgs["idValue"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idValue")) + arg2, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["idValue"] = arg2 + var arg3 bool + if tmp, ok := rawArgs["isPrimary"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isPrimary")) + arg3, err = ec.unmarshalNBoolean2bool(ctx, tmp) + if err != nil { + return nil, err + } + } + args["isPrimary"] = arg3 + return args, nil +} + func (ec *executionContext) field_Mutation_addPermissionsToRole_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4978,6 +5147,347 @@ func (ec *executionContext) _GroupedNavigationActions_secondary(ctx context.Cont return ec.marshalONavigationAction2ᚕgithubᚗcomᚋsavannahghiᚋonboardingᚋpkgᚋonboardingᚋdomainᚐNavigationAction(ctx, field.Selections, res) } +func (ec *executionContext) _Identifier_id(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + 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.(*string) + fc.Result = res + return ec.marshalNString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_clientID(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ClientID, nil + }) + 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_identifierType(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IdentifierType, nil + }) + 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.(enums.IdentifierType) + fc.Result = res + return ec.marshalNIdentifierType2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierType(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_identifierUse(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IdentifierUse, nil + }) + 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.(enums.IdentifierUse) + fc.Result = res + return ec.marshalNIdentifierUse2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierUse(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_identifierValue(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IdentifierValue, nil + }) + 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_description(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_validFrom(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ValidFrom, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*scalarutils.Date) + fc.Result = res + return ec.marshalODate2ᚖgithubᚗcomᚋsavannahghiᚋscalarutilsᚐDate(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_validTo(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ValidTo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*scalarutils.Date) + fc.Result = res + return ec.marshalODate2ᚖgithubᚗcomᚋsavannahghiᚋscalarutilsᚐDate(ctx, field.Selections, res) +} + +func (ec *executionContext) _Identifier_active(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Active, nil + }) + 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) _Identifier_isPrimaryIdentifier(ctx context.Context, field graphql.CollectedField, obj *domain1.Identifier) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Identifier", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsPrimaryIdentifier, nil + }) + 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) _Link_ID(ctx context.Context, field graphql.CollectedField, obj *feedlib.Link) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5352,6 +5862,48 @@ func (ec *executionContext) _Mutation_registerClientUser(ctx context.Context, fi return ec.marshalNClientUserProfile2ᚖgithubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋdomainᚐClientUserProfile(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_addIdentifier(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_addIdentifier_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().AddIdentifier(rctx, args["clientID"].(string), args["idType"].(enums.IdentifierType), args["idValue"].(string), args["isPrimary"].(bool)) + }) + 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.(*domain1.Identifier) + fc.Result = res + return ec.marshalNIdentifier2ᚖgithubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋdomainᚐIdentifier(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_createFacility(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -12743,6 +13295,69 @@ func (ec *executionContext) _GroupedNavigationActions(ctx context.Context, sel a return out } +var identifierImplementors = []string{"Identifier"} + +func (ec *executionContext) _Identifier(ctx context.Context, sel ast.SelectionSet, obj *domain1.Identifier) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, identifierImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Identifier") + case "id": + out.Values[i] = ec._Identifier_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "clientID": + out.Values[i] = ec._Identifier_clientID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "identifierType": + out.Values[i] = ec._Identifier_identifierType(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "identifierUse": + out.Values[i] = ec._Identifier_identifierUse(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "identifierValue": + out.Values[i] = ec._Identifier_identifierValue(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "description": + out.Values[i] = ec._Identifier_description(ctx, field, obj) + case "validFrom": + out.Values[i] = ec._Identifier_validFrom(ctx, field, obj) + case "validTo": + out.Values[i] = ec._Identifier_validTo(ctx, field, obj) + case "active": + out.Values[i] = ec._Identifier_active(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "isPrimaryIdentifier": + out.Values[i] = ec._Identifier_isPrimaryIdentifier(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var linkImplementors = []string{"Link"} func (ec *executionContext) _Link(ctx context.Context, sel ast.SelectionSet, obj *feedlib.Link) graphql.Marshaler { @@ -12839,6 +13454,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "addIdentifier": + out.Values[i] = ec._Mutation_addIdentifier(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "createFacility": out.Values[i] = ec._Mutation_createFacility(ctx, field) if out.Values[i] == graphql.Null { @@ -14582,6 +15202,40 @@ func (ec *executionContext) marshalNID2ᚕstringᚄ(ctx context.Context, sel ast return ret } +func (ec *executionContext) marshalNIdentifier2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋdomainᚐIdentifier(ctx context.Context, sel ast.SelectionSet, v domain1.Identifier) graphql.Marshaler { + return ec._Identifier(ctx, sel, &v) +} + +func (ec *executionContext) marshalNIdentifier2ᚖgithubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋdomainᚐIdentifier(ctx context.Context, sel ast.SelectionSet, v *domain1.Identifier) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._Identifier(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNIdentifierType2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierType(ctx context.Context, v interface{}) (enums.IdentifierType, error) { + var res enums.IdentifierType + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNIdentifierType2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierType(ctx context.Context, sel ast.SelectionSet, v enums.IdentifierType) graphql.Marshaler { + return v +} + +func (ec *executionContext) unmarshalNIdentifierUse2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierUse(ctx context.Context, v interface{}) (enums.IdentifierUse, error) { + var res enums.IdentifierUse + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNIdentifierUse2githubᚗcomᚋsavannahghiᚋonboardingᚑserviceᚋpkgᚋonboardingᚋapplicationᚋenumsᚐIdentifierUse(ctx context.Context, sel ast.SelectionSet, v enums.IdentifierUse) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) { res, err := graphql.UnmarshalInt(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/onboarding/presentation/graph/types.graphql b/pkg/onboarding/presentation/graph/types.graphql index bef7be8b..ca02b6b3 100644 --- a/pkg/onboarding/presentation/graph/types.graphql +++ b/pkg/onboarding/presentation/graph/types.graphql @@ -7,7 +7,6 @@ type Facility { description: String! } - type PIN { user: String! pin: String! @@ -39,12 +38,12 @@ type User { type Contact { ID: String! - Type: ContactType! - Contact: String! #TODO Validate: phones are E164, emails are valid - Active: Boolean! + Type: ContactType! + Contact: String! #TODO Validate: phones are E164, emails are valid + Active: Boolean! #a user may opt not to be contacted via this contact #e.g if it's a shared phone owned by a teenager - OptedIn: Boolean! + OptedIn: Boolean! } type StaffProfile { @@ -79,3 +78,16 @@ type ClientUserProfile { user: User! client: ClientProfile! } + +type Identifier { + id: String! + clientID: String! + identifierType: IdentifierType! + identifierUse: IdentifierUse! + identifierValue: String! + description: String + validFrom: Date + validTo: Date + active: Boolean! + isPrimaryIdentifier: Boolean! +} diff --git a/pkg/onboarding/usecases/client/client.go b/pkg/onboarding/usecases/client/client.go index 1cc53922..e284b3e1 100644 --- a/pkg/onboarding/usecases/client/client.go +++ b/pkg/onboarding/usecases/client/client.go @@ -4,6 +4,7 @@ import ( "context" "github.com/savannahghi/onboarding-service/pkg/onboarding/application/dto" + "github.com/savannahghi/onboarding-service/pkg/onboarding/application/enums" "github.com/savannahghi/onboarding-service/pkg/onboarding/domain" "github.com/savannahghi/onboarding-service/pkg/onboarding/infrastructure" ) @@ -30,9 +31,8 @@ type IRegisterClient interface { // IAddClientIdentifier ... type IAddClientIdentifier interface { - // TODO idType is an enum // TODO use idType and settings to decide if it's a primary identifier or not - AddIdentifier(clientID string, idType string, idValue string, isPrimary bool) (*domain.Identifier, error) + AddIdentifier(ctx context.Context, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) (*domain.Identifier, error) } // IInactivateClient ... @@ -132,8 +132,8 @@ func (cl *UseCasesClientImpl) RegisterClient( } // AddIdentifier stages and adds client identifiers -func (cl *UseCasesClientImpl) AddIdentifier(clientID string, idType string, idValue string, isPrimary bool) (*domain.Identifier, error) { - return nil, nil +func (cl *UseCasesClientImpl) AddIdentifier(ctx context.Context, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) (*domain.Identifier, error) { + return cl.Infrastructure.AddIdentifier(ctx, clientID, idType, idValue, isPrimary) } // InactivateClient makes a client inactive and removes the client from the list of active users diff --git a/pkg/onboarding/usecases/client/client_test.go b/pkg/onboarding/usecases/client/client_test.go index af73718f..1f78f1a8 100644 --- a/pkg/onboarding/usecases/client/client_test.go +++ b/pkg/onboarding/usecases/client/client_test.go @@ -39,3 +39,72 @@ func TestUseCasesClientImplIntegration_RegisterClient(t *testing.T) { // TODO: Try creating the same user twice, should throw an error after we check for uniqueness } + +func TestUseCasesClientImpl_AddIdentifier(t *testing.T) { + ctx := context.Background() + f := testInfrastructureInteractor + + userPayload := &dto.UserInput{ + FirstName: "FirstName", + LastName: "Last Name", + Username: "User Name", + MiddleName: "Middle Name", + DisplayName: "Display Name", + Gender: enumutils.GenderMale, + } + + clientPayload := &dto.ClientProfileInput{ + ClientType: enums.ClientTypeOvc, + } + client, err := f.RegisterClient(ctx, userPayload, clientPayload) + if err != nil { + t.Errorf("failed to create client: %v", err) + return + } + + type args struct { + ctx context.Context + clientID string + idType enums.IdentifierType + idValue string + isPrimary bool + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Happy Case - Successfully add identifier", + args: args{ + ctx: ctx, + clientID: *client.Client.ID, + idType: enums.IdentifierTypeCCC, + idValue: "12345", + isPrimary: true, + }, + wantErr: false, + }, + { + name: "Sad Case - Fail to add identifier", + args: args{ + ctx: ctx, + clientID: "non-existent", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := f.AddIdentifier(tt.args.ctx, tt.args.clientID, tt.args.idType, tt.args.idValue, tt.args.isPrimary) + if (err != nil) != tt.wantErr { + t.Errorf("UseCasesClientImpl.AddIdentifier() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got == nil { + t.Errorf("expected a response but got: %v", got) + return + } + }) + } +} diff --git a/pkg/onboarding/usecases/mock/usecase_mock.go b/pkg/onboarding/usecases/mock/usecase_mock.go index 2225fce0..f23f6ede 100644 --- a/pkg/onboarding/usecases/mock/usecase_mock.go +++ b/pkg/onboarding/usecases/mock/usecase_mock.go @@ -20,6 +20,7 @@ type CreateMock struct { RegisterStaffUserFn func(ctx context.Context, user *dto.UserInput, staff *dto.StaffProfileInput) (*domain.StaffUserProfile, error) SetUserPINFn func(ctx context.Context, input *domain.UserPIN) (bool, error) RegisterClientFn func(ctx context.Context, userInput *dto.UserInput, clientInput *dto.ClientProfileInput) (*domain.ClientUserProfile, error) + AddIdentifierFn func(ctx context.Context, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) (*domain.Identifier, error) } // NewCreateMock creates in itializes create type mocks @@ -143,13 +144,19 @@ func (f *CreateMock) RegisterClient( return f.RegisterClientFn(ctx, userInput, clientInput) } +// AddIdentifier mocks the implementation of `gorm's` AddIdentifier method +func (f *CreateMock) AddIdentifier(ctx context.Context, clientID string, idType enums.IdentifierType, idValue string, isPrimary bool) (*domain.Identifier, error) { + return f.AddIdentifierFn(ctx, clientID, idType, idValue, isPrimary) +} + // QueryMock is a mock of the query methods type QueryMock struct { - RetrieveFacilityFn func(ctx context.Context, id *string, isActive bool) (*domain.Facility, error) - RetrieveFacilityByMFLCodeFn func(ctx context.Context, MFLCode string, isActive bool) (*domain.Facility, error) - GetFacilitiesFn func(ctx context.Context) ([]*domain.Facility, error) - GetUserPINByUserIDFn func(ctx context.Context, userID string) (*domain.UserPIN, error) - GetUserProfileByUserIDFn func(ctx context.Context, userID string, flavour string) (*domain.User, error) + RetrieveFacilityFn func(ctx context.Context, id *string, isActive bool) (*domain.Facility, error) + RetrieveFacilityByMFLCodeFn func(ctx context.Context, MFLCode string, isActive bool) (*domain.Facility, error) + GetFacilitiesFn func(ctx context.Context) ([]*domain.Facility, error) + GetUserPINByUserIDFn func(ctx context.Context, userID string) (*domain.UserPIN, error) + GetUserProfileByUserIDFn func(ctx context.Context, userID string, flavour string) (*domain.User, error) + GetClientProfileByClientIDFn func(ctx context.Context, clientID string) (*domain.ClientProfile, error) } // NewQueryMock initializes a new instance of `GormMock` then mocking the case of success. @@ -245,6 +252,11 @@ func (f *QueryMock) GetUserProfileByUserID(ctx context.Context, userID string, f return f.GetUserProfileByUserIDFn(ctx, userID, flavour) } +// GetClientProfileByClientID defines a mock for fetching a client profile using the client's ID +func (f *QueryMock) GetClientProfileByClientID(ctx context.Context, clientID string) (*domain.ClientProfile, error) { + return f.GetClientProfileByClientIDFn(ctx, clientID) +} + // UpdateMock ... type UpdateMock struct { UpdateUserLastSuccessfulLoginFn func(ctx context.Context, userID string, lastLoginTime time.Time, flavour string) error