Skip to content

Commit

Permalink
feat: implement adding client identifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
Salaton committed Oct 25, 2021
1 parent dd76b3f commit 9f671dd
Show file tree
Hide file tree
Showing 21 changed files with 1,233 additions and 44 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions pkg/onboarding/application/enums/identifier.go
Original file line number Diff line number Diff line change
@@ -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()))
}
22 changes: 11 additions & 11 deletions pkg/onboarding/domain/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
13 changes: 13 additions & 0 deletions pkg/onboarding/infrastructure/database/postgres/gorm/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ type Create interface {
userInput *User,
clientInput *ClientProfile,
) (*ClientUserProfile, error)

AddIdentifier(
ctx context.Context,
identifier *Identifier,
) (*Identifier, error)
}

// GetOrCreateFacility ...
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
11 changes: 11 additions & 0 deletions pkg/onboarding/infrastructure/database/postgres/gorm/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
36 changes: 35 additions & 1 deletion pkg/onboarding/infrastructure/database/postgres/gorm/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down Expand Up @@ -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{},
Expand All @@ -326,6 +359,7 @@ func allTables() []interface{} {
&Contact{},
&StaffProfile{},
&ClientProfile{},
&Identifier{},
&UserAddress{},
&PINData{},
}
Expand Down
41 changes: 41 additions & 0 deletions pkg/onboarding/infrastructure/database/postgres/mappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

0 comments on commit 9f671dd

Please sign in to comment.