diff --git a/.env.example b/.env.example index e4ba1e23..1e3cf161 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,7 @@ REDIS_PORT=6379 JWT_SECRET_KEY=hskdhfjfkdhsgafgwterurorhfh UNIT21_API_KEY= UNIT21_URL=https://sandbox2-api.unit21.com/v1/ +UNIT21_ORG_NAME=String TWILIO_ACCOUNT_SID=AC034879a536d54325687e48544403cb4d TWILIO_AUTH_TOKEN= TWILIO_SMS_SID=MG367a4f51ea6f67a28db4d126eefc734f diff --git a/api/api.go b/api/api.go index 67c37e5a..4c629f2f 100644 --- a/api/api.go +++ b/api/api.go @@ -47,7 +47,7 @@ func baseMiddleware(logger *zerolog.Logger, e *echo.Echo) { func authRoute(config APIConfig, e *echo.Echo) service.Auth { a := repository.NewAuth(config.Redis, config.DB) u := repository.NewUser(config.DB) - c := repository.NewUserContact(config.DB) + c := repository.NewContact(config.DB) service := service.NewAuth(a, u, c) handler := handler.NewAuth(service) handler.RegisterRoutes(e.Group("/auth")) @@ -57,7 +57,7 @@ func authRoute(config APIConfig, e *echo.Echo) service.Auth { func platformRoute(config APIConfig, e *echo.Echo) { p := repository.NewPlatform(config.DB) a := repository.NewAuth(config.Redis, config.DB) - c := repository.NewUserContact(config.DB) + c := repository.NewContact(config.DB) service := service.NewPlatform(p, c, a) handler := handler.NewPlatform(service) handler.RegisterRoutes(e.Group("/platform"), middleware.BearerAuth()) @@ -95,6 +95,6 @@ func verificationRoute(config APIConfig, e *echo.Echo) { Instrument: repository.NewInstrument(config.DB), } service := service.NewUser(repos) - handler := handler.NewUser(e, service) + handler := handler.NewVerification(e, service) handler.RegisterRoutes(e.Group("/verification")) } diff --git a/migrations/0001_string-user_platform_asset_network.sql b/migrations/0001_string-user_platform_asset_network.sql index 26dd5cd6..62974132 100644 --- a/migrations/0001_string-user_platform_asset_network.sql +++ b/migrations/0001_string-user_platform_asset_network.sql @@ -108,26 +108,26 @@ CREATE INDEX network_gas_token_id_fk ON network (gas_token_id); -- ASSET ---------------------------------------------------------------- DROP TRIGGER IF EXISTS update_asset_updated_at ON asset; DROP INDEX IF EXISTS network_gas_token_id_fk; -DROP TABLE asset; +DROP TABLE IF EXISTS asset; ------------------------------------------------------------------------- -- NETWORK -------------------------------------------------------------- DROP TRIGGER IF EXISTS update_network_updated_at ON network; -DROP TABLE network; +DROP TABLE IF EXISTS network; ------------------------------------------------------------------------- -- PLATFORM ------------------------------------------------------------- DROP TRIGGER IF EXISTS update_platform_updated_at ON platfom; -DROP TABLE platform; +DROP TABLE IF EXISTS platform; ------------------------------------------------------------------------- -- STRING_USER ---------------------------------------------------------- DROP TRIGGER IF EXISTS update_string_user_updated_at ON string_user; -DROP TABLE string_user; +DROP TABLE IF EXISTS string_user; ------------------------------------------------------------------------- -- UPDATE_UPDATED_AT_COLUMN() ------------------------------------------- -DROP FUNCTION update_updated_at_column; +DROP FUNCTION IF EXISTS update_updated_at_column; ------------------------------------------------------------------------- -- UUID EXTENSION ------------------------------------------------------- diff --git a/migrations/0002_device_contact_location_instrument.sql b/migrations/0002_user-platform_device_contact_location_instrument.sql similarity index 86% rename from migrations/0002_device_contact_location_instrument.sql rename to migrations/0002_user-platform_device_contact_location_instrument.sql index 6d73825d..9dcc36b7 100644 --- a/migrations/0002_device_contact_location_instrument.sql +++ b/migrations/0002_user-platform_device_contact_location_instrument.sql @@ -1,6 +1,15 @@ ------------------------------------------------------------------------- -- +goose Up +------------------------------------------------------------------------- +-- USER_PLATFORM -------------------------------------------------------- +CREATE TABLE user_platform ( + user_id UUID REFERENCES string_user (id), + platform_id UUID REFERENCES platform (id) +); + +CREATE UNIQUE INDEX user_platform_user_id_platform_id_idx ON user_platform(user_id, platform_id); + ------------------------------------------------------------------------- -- DEVICE --------------------------------------------------------------- CREATE TABLE device ( @@ -13,7 +22,7 @@ CREATE TABLE device ( type TEXT DEFAULT '', -- enum: to be defined at struct level in Go description TEXT DEFAULT '', fingerprint TEXT DEFAULT '', - ip_addresses JSONB DEFAULT '[]'::JSONB, + ip_addresses TEXT[] DEFAULT NULL, user_id UUID NOT NULL REFERENCES string_user (id) ); @@ -96,19 +105,23 @@ EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- -- INSTRUMENT ----------------------------------------------------------- DROP TRIGGER IF EXISTS update_instrument_updated_at ON instrument; -DROP TABLE instrument; +DROP TABLE IF EXISTS instrument; ------------------------------------------------------------------------- -- LOCATION ----------------------------------------------------------- DROP TRIGGER IF EXISTS update_location_updated_at ON location; -DROP TABLE location; +DROP TABLE IF EXISTS location; ------------------------------------------------------------------------- -- CONTACT -------------------------------------------------------------- DROP TRIGGER IF EXISTS update_contact_updated_at ON contact; -DROP TABLE contact; +DROP TABLE IF EXISTS contact; ------------------------------------------------------------------------- -- DEVICE --------------------------------------------------------------- DROP TRIGGER IF EXISTS update_device_updated_at ON device; -DROP TABLE device; \ No newline at end of file +DROP TABLE IF EXISTS device; + +------------------------------------------------------------------------- +-- USER_PLATFORM ----------------------------------------------------- +DROP TABLE IF EXISTS user_platform; \ No newline at end of file diff --git a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql index a99c8d89..861c2f72 100644 --- a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql +++ b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql @@ -93,17 +93,17 @@ EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- -- TRANSACTION ---------------------------------------------------------- DROP TRIGGER IF EXISTS update_transaction_updated_at ON transaction; -DROP TABLE transaction; +DROP TABLE IF EXISTS transaction; ------------------------------------------------------------------------- -- TX_LEG --------------------------------------------------------------- DROP TRIGGER IF EXISTS update_tx_leg_updated_at ON tx_leg; -DROP TABLE tx_leg; +DROP TABLE IF EXISTS tx_leg; ------------------------------------------------------------------------- -- DEVICE_INSTRUMENT ---------------------------------------------------- -DROP TABLE device_instrument; +DROP TABLE IF EXISTS device_instrument; ------------------------------------------------------------------------- -- CONTACT_PLATFORM ----------------------------------------------------- -DROP TABLE contact_platform; \ No newline at end of file +DROP TABLE IF EXISTS contact_platform; \ No newline at end of file diff --git a/migrations/0004_auth_strategy.sql b/migrations/0004_auth_strategy.sql index d06e95a7..f6667bd9 100644 --- a/migrations/0004_auth_strategy.sql +++ b/migrations/0004_auth_strategy.sql @@ -17,4 +17,4 @@ EXECUTE PROCEDURE update_updated_at_column(); -- +goose Down DROP TRIGGER IF EXISTS update_auth_strategy_updated_at ON auth_strategy; -DROP TABLE auth_strategy; \ No newline at end of file +DROP TABLE IF EXISTS auth_strategy; \ No newline at end of file diff --git a/pkg/internal/common/util.go b/pkg/internal/common/util.go index e8ee94a3..dd9d3866 100644 --- a/pkg/internal/common/util.go +++ b/pkg/internal/common/util.go @@ -3,7 +3,10 @@ package common import ( "crypto/sha256" "encoding/hex" + "log" + "math" "reflect" + "strconv" "github.com/ethereum/go-ethereum/accounts" ethcomm "github.com/ethereum/go-ethereum/common" @@ -29,6 +32,17 @@ func RecoverAddress(message string, signature string) (ethcomm.Address, error) { return crypto.PubkeyToAddress(*recovered), nil } +func BigNumberToFloat(bigNumber string, decimals uint64) (floatReturn float64, err error) { + floatReturn, err = strconv.ParseFloat(bigNumber, 64) + if err != nil { + log.Printf("Failed to convert bigNumber to float: %s", err) + err = StringError(err) + return + } + floatReturn = floatReturn * math.Pow(10, -float64(decimals)) + return +} + func isNil(i interface{}) bool { if i == nil { return true diff --git a/pkg/internal/common/util_test.go b/pkg/internal/common/util_test.go index d88bec39..76cf9530 100644 --- a/pkg/internal/common/util_test.go +++ b/pkg/internal/common/util_test.go @@ -3,6 +3,7 @@ package common import ( "testing" + "github.com/String-xyz/string-api/pkg/model" "github.com/stretchr/testify/assert" ) @@ -12,3 +13,11 @@ func TestRecoverSignature(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC", addr.Hex()) } + +func TestKeysAndValues(t *testing.T) { + mType := "type" + m := model.ContactUpdates{Type: &mType} + names, vals := KeysAndValues(m) + assert.Len(t, names, 1) + assert.Len(t, vals, 1) +} diff --git a/pkg/internal/unit21/base.go b/pkg/internal/unit21/base.go index a61b90f1..6f0649e2 100644 --- a/pkg/internal/unit21/base.go +++ b/pkg/internal/unit21/base.go @@ -3,6 +3,7 @@ package unit21 import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "log" "net/http" @@ -12,25 +13,10 @@ import ( "github.com/String-xyz/string-api/pkg/internal/common" ) -// type Unit21Data interface { -// Create(any) (unit21Id string, err error) -// Update(id string, updates any) (err error) -// } - -// type unit21Data struct { -// repository repository.Transactable -// } - -// func newUnit21Data(repo repository.Transactable) Unit21Data { -// return &unit21Data{repository: repo} -// } - -// u21type == entities -// jsonBody = MapUserToEntity(user) func create(datatype string, jsonBody any) (body []byte, err error) { apiKey := os.Getenv("UNIT21_API_KEY") url := os.Getenv("UNIT21_URL") + datatype + "/create" - log.Printf("url: %s", url) + reqBodyBytes, err := json.Marshal(jsonBody) if err != nil { log.Printf("Could not encode %s to bytes: %s", datatype, err) @@ -54,8 +40,6 @@ func create(datatype string, jsonBody any) (body []byte, err error) { res, err := client.Do(req) if err != nil { log.Printf("Request failed to create %s: %s", datatype, err) - //handle 409 on update that is not allowed - //handle 423, 500, 503 for retries return nil, common.StringError(err) } @@ -69,52 +53,61 @@ func create(datatype string, jsonBody any) (body []byte, err error) { log.Printf("String of body from response: %s", string(body)) + if res.StatusCode != 200 { + log.Printf("Request failed to create %s: %s", datatype, fmt.Sprint(res.StatusCode)) + err = common.StringError(fmt.Errorf("request failed with status code %s and return body: %s", fmt.Sprint(res.StatusCode), string(body))) + return + } + return body, nil } -func update(datatype string, id string, updates any) (body []byte, err error) { - // apiKey := os.Getenv("UNIT21_API_KEY") - // url := os.Getenv("UNIT21_URL") + "/" + datatype + "/" + id + "/update" +func update(datatype string, id string, jsonBody any) (body []byte, err error) { + apiKey := os.Getenv("UNIT21_API_KEY") + orgName := os.Getenv("UNIT21_ORG_NAME") + url := os.Getenv("UNIT21_URL") + orgName + "/" + datatype + "/" + id + "/update" - // names, keyToUpdate := common.KeysAndValues(updates) + reqBodyBytes, err := json.Marshal(jsonBody) + if err != nil { + log.Printf("Could not encode %s to bytes: %s", datatype, err) + return nil, common.StringError(err) + } - // reqBodyBytes, err := json.Marshal(jsonBody) - // if err != nil { - // log.Printf("Could not encode %s to bytes: %s", datatype, err) - // return - // } + bodyReader := bytes.NewReader(reqBodyBytes) - // bodyReader := bytes.NewReader(reqBodyBytes) + req, err := http.NewRequest(http.MethodPut, url, bodyReader) + if err != nil { + log.Printf("Could not create request for %s: %s", datatype, err) + return nil, common.StringError(err) + } - // req, err := http.NewRequest(http.MethodPost, url, bodyReader) - // if err != nil { - // log.Printf("Could not create request for %s: %s", datatype, err) - // return - // } + req.Header.Add("accept", "application/json") + req.Header.Add("content-type", "application/json") + req.Header.Add("u21-key", apiKey) - // req.Header.Add("accept", "application/json") - // req.Header.Add("content-type", "application/json") - // req.Header.Add("u21-key", apiKey) + client := http.Client{Timeout: 10 * time.Second} - // client := http.Client{Timeout: 10 * time.Second} + res, err := client.Do(req) + if err != nil { + log.Printf("Request failed to update %s: %s", datatype, err) + return nil, common.StringError(err) + } - // res, err := client.Do(req) - // if err != nil { - // log.Printf("Request failed to create %s: %s", datatype, err) - // //handle 409 on update that is not allowed - // //handle 423, 500, 503 for retries - // return - // } + defer res.Body.Close() - // defer res.Body.Close() + body, err = ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Error extracting body from %s update request: %s", datatype, err) + return nil, common.StringError(err) + } - // body, err = ioutil.ReadAll(res.Body) - // if err != nil { - // log.Printf("Error extracting body from %s create request: %s", datatype, err) - // return - // } + log.Printf("String of body from response: %s", string(body)) - // log.Printf("String of body from response: %s", string(body)) + if res.StatusCode != 200 { + log.Printf("Request failed to update %s: %s", datatype, fmt.Sprint(res.StatusCode)) + err = common.StringError(fmt.Errorf("request failed with status code %s and return body: %s", fmt.Sprint(res.StatusCode), string(body))) + return + } - return + return body, nil } diff --git a/pkg/internal/unit21/entities_api.go b/pkg/internal/unit21/entities_api.go deleted file mode 100644 index 3deaea07..00000000 --- a/pkg/internal/unit21/entities_api.go +++ /dev/null @@ -1,76 +0,0 @@ -package unit21 - -import ( - "encoding/json" - "log" - - "github.com/String-xyz/string-api/pkg/internal/common" - "github.com/String-xyz/string-api/pkg/model" - "github.com/String-xyz/string-api/pkg/repository" -) - -type Entity interface { - Create(user model.User) (unit21Id string, err error) - Update(id string, updates any) (err error) -} - -type entity struct { - userRepo repository.User - // deviceRepo repository.Device - contactRepo repository.UserContact - // instrumentRepo repository.Instrument -} - -// With Device and Instrument -// func newEntity(user repository.User, device repository.Device, contact repository.UserContact, instrument repository.Instrument) Entity { -// return &entity{userRepo: user, deviceRepo: device, contactRepo: contact, instrumentRepo: instrument} -// } - -func NewEntity(user repository.User, contact repository.UserContact) Entity { - return &entity{userRepo: user, contactRepo: contact} -} - -// // -// u21entity := unit21.NewEntity() -// u21insturment := unit21.NewIntrument() - -// u21entity.Create(user) -// // - -func (e entity) Create(user model.User) (unit21Id string, err error) { - - // ultimately may want a join here. - // devices, err := e.deviceRepo.ListUserID(user.ID, 100, 0) - // instruments, err := e.instrumentRepo.ListUserID(user.ID, 100, 0) - _, err = e.contactRepo.ListUserID(user.ID, 100, 0) - if err != nil { - return "", common.StringError(err) - } - - body, err := create("entities", MapUserToEntity(user)) - if err != nil { - return "", common.StringError(err) - } - - var entity *entityResponse - err = json.Unmarshal(body, &entity) - if err != nil { - log.Printf("Reading body failed: %s", err) - return "", common.StringError(err) - } - - log.Printf("Unit21Id: %s", entity.Unit21Id) - return entity.Unit21Id, nil -} - -func (e entity) Update(id string, updates any) (err error) { - - _, err = update("entities", id, updates) - - if err != nil { - log.Printf("Reading body failed: %s", err) - return common.StringError(err) - } - - return nil -} diff --git a/pkg/internal/unit21/entities_mapper.go b/pkg/internal/unit21/entities_mapper.go deleted file mode 100644 index 688e2fbb..00000000 --- a/pkg/internal/unit21/entities_mapper.go +++ /dev/null @@ -1,42 +0,0 @@ -package unit21 - -import "github.com/String-xyz/string-api/pkg/model" - -func MapUserToEntity(user model.User) *u21entity { - var userTagArr []string - // Temporarily disabled - // if user.Tags != nil { - // for key, value := range user.Tags { - // userTagArr = append(userTagArr, key+":"+value) - // } - // } - - jsonBody := &u21entity{ - GeneralData: &general{ - EntityId: user.ID, - EntityType: "user", - Status: user.Status, - RegisteredAt: int(user.CreatedAt.Unix()), - Tags: userTagArr, // convert from jsonb into array of key:value string pairs - }, - UserData: &personal{ - FirstName: user.FirstName, - MiddleName: user.MiddleName, - LastName: user.LastName, - }, - CommunicationData: &communication{ - Emails: nil, //data.emails, //might need to be converted to []string - Phones: nil, //data.phones, //might need to be converted to []string - }, - DigitalData: &digitalInfo{ - IpAddresses: nil, //data.ipAddresses, //might need to be converted to []string - ClientFingerprints: nil, //data.fingerprints, //schema doesn't have a fingerprint, might need to be convered to []string - }, - CustomData: nil, //&custom{ - // Platform: nil,//data.partnerName, - // }, - // add WorkflowOptions if not default? - } - - return jsonBody -} diff --git a/pkg/internal/unit21/entities_test.go b/pkg/internal/unit21/entities_test.go deleted file mode 100644 index 4d9b4852..00000000 --- a/pkg/internal/unit21/entities_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package unit21 - -import ( - "log" - "testing" - "time" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/String-xyz/string-api/pkg/model" - "github.com/String-xyz/string-api/pkg/repository" - "github.com/google/uuid" - "github.com/jmoiron/sqlx" - "github.com/joho/godotenv" - "github.com/stretchr/testify/assert" -) - -func TestCreateEntity(t *testing.T) { - err := godotenv.Load("../../../.env") - assert.NoError(t, err) - - id := uuid.NewString() - db, _, err := sqlmock.New() - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) - } - defer db.Close() - - user := model.User{ - ID: id, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - DeactivatedAt: nil, - Type: "User", - Status: "Onboarded", - Tags: nil, - FirstName: "Test", - MiddleName: "A", - LastName: "User", - } - - // Dependent on Device and Instrument Repos being created - userRepo := repository.NewUser(sqlxDB) - // deviceRepo := repository.NewDevice(sqlxDB) - contactRepo := repository.NewUserContact(sqlxDB) - // instrumentRepo := repository.NewInstrument(sqlxDB) - - // u21Entity := newEntity(userRepo, deviceRepo, contactRepo, instrumentRepo) - u21Entity := NewEntity(userRepo, contactRepo) - - u21EntityId, err := u21Entity.Create(user) - assert.NoError(t, err) - log.Printf("u21EntityId: %s", u21EntityId) - assert.Greater(t, len([]rune(u21EntityId)), 0) - - //validate response from Unit21 - //check Unit21 dashboard for new entity added - // todo: mock call to client once it's manually tested -} diff --git a/pkg/internal/unit21/entities_types.go b/pkg/internal/unit21/entities_types.go deleted file mode 100644 index 164cad61..00000000 --- a/pkg/internal/unit21/entities_types.go +++ /dev/null @@ -1,57 +0,0 @@ -package unit21 - -type u21entity struct { - GeneralData *general `json:"general_data"` - UserData *personal `json:"user_data,omitempty"` - CommunicationData *communication `json:"communication_data,omitempty"` - DigitalData *digitalInfo `json:"digital_data,omitempty"` - CustomData *custom `json:"custom_data,omitempty"` - WorkflowOptions *options `json:"options,omitempty"` -} - -type general struct { - EntityId string `json:"entity_id"` - EntityType string `json:"entity_type"` //employee or business - says user in the spreadsheet? - EntitySubType string `json:"entity_subtype"` - Status string `json:"status,omitempty"` - RegisteredAt int `json:"registered_at"` //date in seconds since 1/1/1970 - Tags []string `json:"tags,omitempty"` //list of format: keyString:valueString -} - -type personal struct { - FirstName string `json:"first_name"` - MiddleName string `json:"middle_name,omitempty"` - LastName string `json:"last_name"` - BirthDay int `json:"day_of_birth,omitempty"` - BirthMonth int `json:"month_of_birth,omitempty"` - BirthYear int `json:"year_of_birth,omitempty"` - SSN string `json:"ssn,omitempty"` -} - -type communication struct { - Emails []string `json:"email_addresses,omitempty"` - Phones []string `json:"phone_numbers,omitempty"` //E.164 format +12125551395 ( '[+][country code][area code][local phone number]' https://en.wikipedia.org/wiki/E.164 -} - -type digitalInfo struct { - IpAddresses []string `json:"ip_addresses,omitempty"` //ipv4 or ipv6 - ClientFingerprints []string `json:"client_fingerprints,omitempty"` -} - -type custom struct { - //more can be added to this as needed - Platforms []string `json:"platforms,omitempty"` //where does this come from? do you want it called partnerName instead? -} - -type options struct { - ResolveGeoIp bool `json:"resolve_geoip"` //default true - MergeCustomData bool `json:"merge_custom_data"` //default false https://docs.unit21.ai/docs/how-data-merges-on-updates#custom-data-merge-strategy - UpsertOnConflict bool `json:"upsert_on_conflict"` //default true BUT should we change to false? don't need update endpoint if allowed -} - -type entityResponse struct { - Ignored bool `json:"ignored,omitempty"` - EntityId string `json:"entity_id"` - PreviouslyExisted bool `json:"previously_existed"` - Unit21Id string `json:"unit21_id"` -} diff --git a/pkg/internal/unit21/entity.go b/pkg/internal/unit21/entity.go new file mode 100644 index 00000000..8301c25c --- /dev/null +++ b/pkg/internal/unit21/entity.go @@ -0,0 +1,255 @@ +package unit21 + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" + + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" +) + +type Entity interface { + Create(user model.User) (unit21Id string, err error) + Update(user model.User) (unit21Id string, err error) + AddInstruments(entityId string, instrumentId []string) (err error) +} + +type EntityRepos struct { + device repository.Device + contact repository.Contact + userPlatform repository.UserPlatform +} + +type entity struct { + repo EntityRepos +} + +func NewEntity(r EntityRepos) Entity { + return &entity{repo: r} +} + +// https://docs.unit21.ai/reference/create_entity +func (e entity) Create(user model.User) (unit21Id string, err error) { + + // ultimately may want a join here. + + communications, err := getCommunications(user.ID, e.repo.contact) + if err != nil { + log.Printf("Failed to gather Unit21 entity communications: %s", err) + return "", common.StringError(err) + } + + digitalData, err := getEntityDigitalData(user.ID, e.repo.device) + if err != nil { + log.Printf("Failed to gather Unit21 entity digitalData: %s", err) + return "", common.StringError(err) + } + + customData, err := getCustomData(user.ID, e.repo.userPlatform) + if err != nil { + log.Printf("Failed to gather Unit21 entity customData: %s", err) + return "", common.StringError(err) + } + + body, err := create("entities", mapUserToEntity(user, communications, digitalData, customData)) + if err != nil { + log.Printf("Unit21 Entity create failed: %s", err) + return "", common.StringError(err) + } + + var entity *createEntityResponse + err = json.Unmarshal(body, &entity) + if err != nil { + log.Printf("Reading body failed: %s", err) + return "", common.StringError(err) + } + + log.Printf("Unit21Id: %s", entity.Unit21Id) + return entity.Unit21Id, nil +} + +// https://docs.unit21.ai/reference/update_entity +func (e entity) Update(user model.User) (unit21Id string, err error) { + + // ultimately may want a join here. + + communications, err := getCommunications(user.ID, e.repo.contact) + if err != nil { + log.Printf("Failed to gather Unit21 entity communications: %s", err) + err = common.StringError(err) + return + } + + digitalData, err := getEntityDigitalData(user.ID, e.repo.device) + if err != nil { + log.Printf("Failed to gather Unit21 entity digitalData: %s", err) + err = common.StringError(err) + return + } + + customData, err := getCustomData(user.ID, e.repo.userPlatform) + if err != nil { + log.Printf("Failed to gather Unit21 entity customData: %s", err) + err = common.StringError(err) + return + } + + body, err := update("entities", user.ID, mapUserToEntity(user, communications, digitalData, customData)) + if err != nil { + log.Printf("Unit21 Entity create failed: %s", err) + err = common.StringError(err) + return + } + + var entity *updateEntityResponse + err = json.Unmarshal(body, &entity) + if err != nil { + log.Printf("Reading body failed: %s", err) + err = common.StringError(err) + return + } + + log.Printf("Unit21Id: %s", entity.Unit21Id) + return entity.Unit21Id, nil +} + +// https://docs.unit21.ai/reference/add_instruments +func (e entity) AddInstruments(entityId string, instrumentIds []string) (err error) { + apiKey := os.Getenv("UNIT21_API_KEY") + orgName := os.Getenv("UNIT21_ORG_NAME") + url := os.Getenv("UNIT21_URL") + orgName + "/entities/" + entityId + "/add-instruments" + + instruments := make(map[string][]string) + instruments["instrument_ids"] = instrumentIds + reqBodyBytes, err := json.Marshal(instruments) + if err != nil { + log.Printf("Could not encode instrumentIds to bytes: %s", err) + return common.StringError(err) + } + + bodyReader := bytes.NewReader(reqBodyBytes) + + req, err := http.NewRequest(http.MethodPut, url, bodyReader) + if err != nil { + log.Printf("Could not create request for instrumentIds: %s", err) + return common.StringError(err) + } + + req.Header.Add("accept", "application/json") + req.Header.Add("content-type", "application/json") + req.Header.Add("u21-key", apiKey) + + client := http.Client{Timeout: 10 * time.Second} + + res, err := client.Do(req) + if err != nil { + log.Printf("Request failed to create instrumentIds: %s", err) + return common.StringError(err) + } + + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Error extracting return body from instrumentIds add request: %s", err) + return common.StringError(err) + } + if res.StatusCode != 200 { + log.Printf("Request failed to create instrumentIds: %s", fmt.Sprint(res.StatusCode)) + err = fmt.Errorf("request failed with status code %s and return body: %s", fmt.Sprint(res.StatusCode), string(body)) + return common.StringError(err) + } + + log.Printf("String of body from response: %s", string(body)) + + return +} + +func getCommunications(userId string, contact repository.Contact) (communications entityCommunication, err error) { + // Get user contacts + contacts, err := contact.ListByUserId(userId, 100, 0) + if err != nil { + log.Printf("Failed go get user contacts: %s", err) + err = common.StringError(err) + return + } + + // Convert contact structs to an entity communication struct + for _, contact := range contacts { + if contact.Type == "Email" { + communications.Emails = append(communications.Emails, contact.Data) + } else if contact.Type == "Phone" { + communications.Phones = append(communications.Phones, contact.Data) + } + } + log.Printf("communication: %s", communications) + return +} + +func getEntityDigitalData(userId string, device repository.Device) (deviceData entityDigitalData, err error) { + devices, err := device.ListByUserId(userId, 100, 0) + if err != nil { + log.Printf("Failed to get user devices: %s", err) + err = common.StringError(err) + return + } + + for _, device := range devices { + deviceData.IpAddresses = append(deviceData.IpAddresses, device.IpAddresses...) + deviceData.ClientFingerprints = append(deviceData.ClientFingerprints, device.Fingerprint) + } + log.Printf("deviceData: %s", deviceData) + return +} + +func getCustomData(userId string, userPlatform repository.UserPlatform) (customData entityCustomData, err error) { + devices, err := userPlatform.ListByUserId(userId, 100, 0) + if err != nil { + log.Printf("Failed to get user platforms: %s", err) + err = common.StringError(err) + return + } + + for _, platform := range devices { + customData.Platforms = append(customData.Platforms, platform.PlatformID) + } + log.Printf("deviceData: %s", customData) + return +} + +func mapUserToEntity(user model.User, communication entityCommunication, digitalData entityDigitalData, customData entityCustomData) *u21Entity { + var userTagArr []string + if user.Tags != nil { + for key, value := range user.Tags { + userTagArr = append(userTagArr, key+":"+value) + } + } + + jsonBody := &u21Entity{ + GeneralData: &entityGeneral{ + EntityId: user.ID, + EntityType: "user", + Status: user.Status, + RegisteredAt: int(user.CreatedAt.Unix()), + Tags: userTagArr, // convert from jsonb into array of key:value string pairs + }, + UserData: &entityPersonal{ + FirstName: user.FirstName, + MiddleName: user.MiddleName, + LastName: user.LastName, + }, + CommunicationData: &communication, + DigitalData: &digitalData, + CustomData: &customData, + // add WorkflowOptions if not default? + } + + return jsonBody +} diff --git a/pkg/internal/unit21/entity_test.go b/pkg/internal/unit21/entity_test.go new file mode 100644 index 00000000..b338c3a3 --- /dev/null +++ b/pkg/internal/unit21/entity_test.go @@ -0,0 +1,156 @@ +package unit21 + +import ( + "log" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/joho/godotenv" + "github.com/lib/pq" + "github.com/stretchr/testify/assert" +) + +func TestCreateEntity(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + entityId := uuid.NewString() + db, mock, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + user := model.User{ + ID: entityId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeactivatedAt: nil, + Type: "User", + Status: "Onboarded", + Tags: nil, + FirstName: "Test", + MiddleName: "A", + LastName: "User", + } + + mockedContactRow := sqlmock.NewRows([]string{"id", "user_id", "created_at", "updated_at", "last_authenticated_at", "type", "status", "data"}).AddRow(uuid.NewString(), entityId, time.Now(), time.Now(), time.Now(), "email", "verified", "test@gmail.com") + mock.ExpectQuery(`SELECT \* FROM contact WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedContactRow) + + mockedDeviceRow := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "last_used_at", "validated_at", "type", "description", "fingerprint", "ip_addresses", "user_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), time.Now(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"192.0.1.1"}, uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM device WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedDeviceRow) + + mockedUserPlatformRow := sqlmock.NewRows([]string{"user_id", "platform_id"}).AddRow(entityId, uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM user_platform WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedUserPlatformRow) + + repos := EntityRepos{ + device: repository.NewDevice(sqlxDB), + contact: repository.NewContact(sqlxDB), + userPlatform: repository.NewUserPlatform(sqlxDB), + } + + u21Entity := NewEntity(repos) + + u21EntityId, err := u21Entity.Create(user) + assert.NoError(t, err) + log.Printf("u21EntityId: %s", u21EntityId) + assert.Greater(t, len([]rune(u21EntityId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new entity added + // todo: mock call to client once it's manually tested +} + +func TestUpdateEntity(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + // choose an older entity so we can update it + entityId := "d8451ddd-6116-4b62-9072-0e8b63a843f1" + db, mock, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + user := model.User{ + ID: entityId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeactivatedAt: nil, + Type: "User", + Status: "Updated", // changing from Onboarded + Tags: nil, + FirstName: "Test", + MiddleName: "Another", // changing from A + LastName: "User", + } + + mockedContactRow := sqlmock.NewRows([]string{"id", "user_id", "created_at", "updated_at", "last_authenticated_at", "type", "status", "data"}).AddRow(uuid.NewString(), entityId, time.Now(), time.Now(), time.Now(), "email", "verified", "test@gmail.com") + mock.ExpectQuery(`SELECT \* FROM contact WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedContactRow) + + mockedDeviceRow := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "last_used_at", "validated_at", "type", "description", "fingerprint", "ip_addresses", "user_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), time.Now(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"192.0.1.1"}, uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM device WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedDeviceRow) + + mockedUserPlatformRow := sqlmock.NewRows([]string{"user_id", "platform_id"}).AddRow(entityId, uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM user_platform WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedUserPlatformRow) + + repos := EntityRepos{ + device: repository.NewDevice(sqlxDB), + contact: repository.NewContact(sqlxDB), + userPlatform: repository.NewUserPlatform(sqlxDB), + } + + u21Entity := NewEntity(repos) + + // update in u21 + u21EntityId, err := u21Entity.Update(user) + assert.NoError(t, err) + log.Printf("u21EntityId: %s", u21EntityId) + assert.Greater(t, len([]rune(u21EntityId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new entity added + // todo: mock call to client once it's manually tested +} + +func TestAddInstruments(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + entityId := "44142758-f015-4f79-a004-e554b0641480" //previous created test user + var instrumentIds []string + db, _, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + // mock new instrumentIds + for i := 1; i <= 10; i++ { + instrumentIds = append(instrumentIds, uuid.NewString()) + } + + repos := EntityRepos{ + device: repository.NewDevice(sqlxDB), + contact: repository.NewContact(sqlxDB), + userPlatform: repository.NewUserPlatform(sqlxDB), + } + + u21Entity := NewEntity(repos) + + err = u21Entity.AddInstruments(entityId, instrumentIds) + assert.NoError(t, err) + + //validate response from Unit21 + //check Unit21 dashboard for new entity added + // todo: mock call to client once it's manually tested +} diff --git a/pkg/internal/unit21/instrument.go b/pkg/internal/unit21/instrument.go new file mode 100644 index 00000000..df444c10 --- /dev/null +++ b/pkg/internal/unit21/instrument.go @@ -0,0 +1,217 @@ +package unit21 + +import ( + "encoding/json" + "log" + + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" +) + +type Instrument interface { + Create(instrument model.Instrument) (unit21Id string, err error) + Update(instrument model.Instrument) (unit21Id string, err error) +} + +type InstrumentRepo struct { + user repository.User + device repository.Device + location repository.Location +} + +type instrument struct { + repo InstrumentRepo +} + +func NewInstrument(r InstrumentRepo) Instrument { + return &instrument{repo: r} +} + +func (i instrument) Create(instrument model.Instrument) (unit21Id string, err error) { + + source, err := getSource(instrument.UserID, i.repo.user) + if err != nil { + log.Printf("Failed to gather Unit21 instrument source: %s", err) + return "", common.StringError(err) + } + + entities, err := getEntities(instrument.UserID, i.repo.user) + if err != nil { + log.Printf("Failed to gather Unit21 instrument entity: %s", err) + return "", common.StringError(err) + } + + digitalData, err := getInstrumentDigitalData(instrument.UserID, i.repo.device) + if err != nil { + log.Printf("Failed to gather Unit21 entity digitalData: %s", err) + return "", common.StringError(err) + } + + locationData, err := getLocationData(instrument.LocationID.String, i.repo.location) + if err != nil { + log.Printf("Failed to gather Unit21 instrument location: %s", err) + return "", common.StringError(err) + } + + body, err := create("instruments", mapToUnit21Instrument(instrument, source, entities, digitalData, locationData)) + if err != nil { + log.Printf("Unit21 Instrument create failed: %s", err) + return "", common.StringError(err) + } + + var u21Response *createInstrumentResponse + err = json.Unmarshal(body, &u21Response) + if err != nil { + log.Printf("Reading body failed: %s", err) + return "", common.StringError(err) + } + + log.Printf("Unit21Id: %s", u21Response.Unit21Id) + return u21Response.Unit21Id, nil +} + +func (i instrument) Update(instrument model.Instrument) (unit21Id string, err error) { + + source, err := getSource(instrument.UserID, i.repo.user) + if err != nil { + log.Printf("Failed to gather Unit21 instrument source: %s", err) + return "", common.StringError(err) + } + + entities, err := getEntities(instrument.UserID, i.repo.user) + if err != nil { + log.Printf("Failed to gather Unit21 instrument entity: %s", err) + return "", common.StringError(err) + } + + digitalData, err := getInstrumentDigitalData(instrument.UserID, i.repo.device) + if err != nil { + log.Printf("Failed to gather Unit21 entity digitalData: %s", err) + return "", common.StringError(err) + } + + locationData, err := getLocationData(instrument.LocationID.String, i.repo.location) + if err != nil { + log.Printf("Failed to gather Unit21 instrument location: %s", err) + return "", common.StringError(err) + } + + body, err := update("instruments", instrument.ID, mapToUnit21Instrument(instrument, source, entities, digitalData, locationData)) + if err != nil { + log.Printf("Unit21 Instrument create failed: %s", err) + return "", common.StringError(err) + } + + var u21Response *updateInstrumentResponse + err = json.Unmarshal(body, &u21Response) + if err != nil { + log.Printf("Reading body failed: %s", err) + return "", common.StringError(err) + } + + log.Printf("Unit21Id: %s", u21Response.Unit21Id) + + return u21Response.Unit21Id, nil +} + +func getSource(userID string, userRepo repository.User) (source string, err error) { + user, err := userRepo.GetById(userID) + if err != nil { + log.Printf("Failed go get user contacts: %s", err) + return "", common.StringError(err) + } + + if user.Tags["internal"] == "true" { + return "internal", nil + } + return "external", nil +} + +func getEntities(userID string, userRepo repository.User) (entity instrumentEntity, err error) { + user, err := userRepo.GetById(userID) + if err != nil { + log.Printf("Failed go get user contacts: %s", err) + err = common.StringError(err) + return + } + + entity = instrumentEntity{ + EntityId: userID, + RelationshipId: "", + EntityType: user.Type, + } + return entity, nil +} + +func getInstrumentDigitalData(userId string, deviceRepo repository.Device) (digitalData instrumentDigitalData, err error) { + devices, err := deviceRepo.ListByUserId(userId, 100, 0) + if err != nil { + log.Printf("Failed to get user devices: %s", err) + err = common.StringError(err) + return + } + + for _, device := range devices { + digitalData.IpAddresses = append(digitalData.IpAddresses, device.IpAddresses...) + } + log.Printf("deviceData: %s", digitalData) + return +} + +func getLocationData(locationID string, locationRepo repository.Location) (locationData instrumentLocationData, err error) { + location, err := locationRepo.GetById(locationID) + if err != nil { + log.Printf("Failed go get instrument location: %s", err) + err = common.StringError(err) + return + } + + locationData = instrumentLocationData{ + Type: location.Type, + BuildingNumber: location.BuildingNumber, + UnitNumber: location.UnitNumber, + StreetName: location.StreetName, + City: location.City, + State: location.State, + PostalCode: location.PostalCode, + Country: location.Country, + VerifiedOn: int(location.CreatedAt.Unix()), + } + + return locationData, nil +} + +func mapToUnit21Instrument(instrument model.Instrument, source string, entityData instrumentEntity, digitalData instrumentDigitalData, locationData instrumentLocationData) *u21Instrument { + var instrumentTagArr []string + if instrument.Tags != nil { + for key, value := range instrument.Tags { + instrumentTagArr = append(instrumentTagArr, key+":"+value) + } + } + + var entityArray []instrumentEntity + entityArray = append(entityArray, entityData) + + jsonBody := &u21Instrument{ + InstrumentId: instrument.ID, + InstrumentType: instrument.Type, + // InstrumentSubtype: "", + // Source: "internal", + Status: instrument.Status, + RegisteredAt: int(instrument.CreatedAt.Unix()), + ParentInstrumentId: "", + Entities: entityArray, + CustomData: &instrumentCustomData{ + None: nil, + }, + DigitalData: &digitalData, + LocationData: &locationData, + Tags: instrumentTagArr, + // Options: &options, + } + + log.Printf("%+v\n", jsonBody) + + return jsonBody +} diff --git a/pkg/internal/unit21/instrument_test.go b/pkg/internal/unit21/instrument_test.go new file mode 100644 index 00000000..75de7518 --- /dev/null +++ b/pkg/internal/unit21/instrument_test.go @@ -0,0 +1,119 @@ +package unit21 + +import ( + "database/sql" + "log" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/joho/godotenv" + "github.com/lib/pq" + "github.com/stretchr/testify/assert" +) + +func TestCreateInstrument(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + instrumentId := uuid.NewString() + db, mock, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + instrument := model.Instrument{ + ID: instrumentId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeactivatedAt: nil, + Type: "Credit Card", + Status: "Verified", + Tags: nil, + Network: "Visa", + PublicKey: "", + Last4: "1234", + UserID: uuid.NewString(), + LocationID: sql.NullString{String: uuid.NewString()}, + } + + mockedDeviceRow := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "last_used_at", "validated_at", "type", "description", "fingerprint", "ip_addresses", "user_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), time.Now(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"192.0.1.1"}, uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM device WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedDeviceRow) + + mockedLocationRow := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "type", "status", "tags", "building_numeber", "unit_number", "street_name", "city", "state", "postal_code", "country"}).AddRow(uuid.NewString(), time.Now(), time.Now(), "Home", "Verified", nil, "20181", "411", "Lark Avenue", "Somerville", "MA", "01443", "USA") + mock.ExpectQuery(`SELECT \* FROM location WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedLocationRow) + + repo := InstrumentRepo{ + user: repository.NewUser(sqlxDB), + device: repository.NewDevice(sqlxDB), + location: repository.NewLocation(sqlxDB), + } + + u21Instrument := NewInstrument(repo) + + u21InstrumentId, err := u21Instrument.Create(instrument) + assert.NoError(t, err) + log.Printf("u21InstrumentId: %s", u21InstrumentId) + assert.Greater(t, len([]rune(u21InstrumentId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new instrument added + // todo: mock call to client once it's manually tested +} + +func TestUpdateInstrument(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + instrumentId := "bb73ef2d-0e62-4381-8a37-6a32c11fb226" + db, mock, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + instrument := model.Instrument{ + ID: instrumentId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeactivatedAt: nil, + Type: "Credit Card", + Status: "Verified", + Tags: nil, + Network: "Visa", + PublicKey: "", + Last4: "1234", + UserID: uuid.NewString(), + LocationID: sql.NullString{String: uuid.NewString()}, + } + + mockedDeviceRow := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "last_used_at", "validated_at", "type", "description", "fingerprint", "ip_addresses", "user_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), time.Now(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"192.0.1.1"}, uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM device WHERE user_id = (.+) LIMIT (.+) OFFSET (.+)`).WithArgs().WillReturnRows(mockedDeviceRow) + + mockedLocationRow := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "type", "status", "tags", "building_numeber", "unit_number", "street_name", "city", "state", "postal_code", "country"}).AddRow(uuid.NewString(), time.Now(), time.Now(), "Home", "Verified", nil, "20181", "411", "Lark Avenue", "Somerville", "MA", "01443", "USA") + mock.ExpectQuery(`SELECT \* FROM location WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedLocationRow) + + repo := InstrumentRepo{ + user: repository.NewUser(sqlxDB), + device: repository.NewDevice(sqlxDB), + location: repository.NewLocation(sqlxDB), + } + + u21Instrument := NewInstrument(repo) + + u21InstrumentId, err := u21Instrument.Update(instrument) + assert.NoError(t, err) + log.Printf("u21InstrumentId: %s", u21InstrumentId) + assert.Greater(t, len([]rune(u21InstrumentId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new instrument added + // todo: mock call to client once it's manually tested +} diff --git a/pkg/internal/unit21/transaction.go b/pkg/internal/unit21/transaction.go new file mode 100644 index 00000000..f47f104c --- /dev/null +++ b/pkg/internal/unit21/transaction.go @@ -0,0 +1,209 @@ +package unit21 + +import ( + "encoding/json" + "log" + + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" +) + +type Transaction interface { + Create(transaction model.Transaction) (unit21Id string, err error) + Update(transaction model.Transaction) (unit21Id string, err error) +} + +type TransactionRepo struct { + txLeg repository.TxLeg + user repository.User + asset repository.Asset +} + +type transaction struct { + repo TransactionRepo +} + +func NewTransaction(r TransactionRepo) Transaction { + return &transaction{repo: r} +} + +func (i transaction) Create(transaction model.Transaction) (unit21Id string, err error) { + + transactionData, err := getTransactionData(transaction, i.repo.user, i.repo.asset, i.repo.txLeg) + if err != nil { + log.Printf("Failed to gather Unit21 transaction source: %s", err) + return "", common.StringError(err) + } + + body, err := create("events", mapToUnit21Event(transaction, transactionData)) + if err != nil { + log.Printf("Unit21 Transaction create failed: %s", err) + return "", common.StringError(err) + } + + var u21Response *createEventResponse + err = json.Unmarshal(body, &u21Response) + if err != nil { + log.Printf("Reading body failed: %s", err) + return "", common.StringError(err) + } + + log.Printf("Unit21Id: %s", u21Response.Unit21Id) + return u21Response.Unit21Id, nil +} + +func (i transaction) Update(transaction model.Transaction) (unit21Id string, err error) { + + transactionData, err := getTransactionData(transaction, i.repo.user, i.repo.asset, i.repo.txLeg) + if err != nil { + log.Printf("Failed to gather Unit21 transaction source: %s", err) + return "", common.StringError(err) + } + + body, err := update("events", transaction.ID, mapToUnit21Event(transaction, transactionData)) + if err != nil { + log.Printf("Unit21 Transaction create failed: %s", err) + return "", common.StringError(err) + } + + var u21Response *updateEventResponse + err = json.Unmarshal(body, &u21Response) + if err != nil { + log.Printf("Reading body failed: %s", err) + return "", common.StringError(err) + } + + log.Printf("Unit21Id: %s", u21Response.Unit21Id) + return u21Response.Unit21Id, nil +} + +func getTransactionData(transaction model.Transaction, userRepo repository.User, assetRepo repository.Asset, txLegRepo repository.TxLeg) (txData transactionData, err error) { + senderData, err := txLegRepo.GetById(transaction.OriginTxLegID) + if err != nil { + log.Printf("Failed go get origin transaction leg: %s", err) + err = common.StringError(err) + return + } + + receiverData, err := txLegRepo.GetById(transaction.DestinationTxLegID) + if err != nil { + log.Printf("Failed go get origin transaction leg: %s", err) + err = common.StringError(err) + return + } + + receiverType, err := getSource(receiverData.UserID, userRepo) + if err != nil { + log.Printf("Failed to gather Unit21 transaction receiver user source: %s", err) + err = common.StringError(err) + return + } + + senderType, err := getSource(senderData.UserID, userRepo) + if err != nil { + log.Printf("Failed to gather Unit21 transaction sender user source: %s", err) + err = common.StringError(err) + return + } + + senderAsset, err := assetRepo.GetById(senderData.AssetID) + if err != nil { + log.Printf("Failed go get transaction sender asset: %s", err) + err = common.StringError(err) + return + } + + receiverAsset, err := assetRepo.GetById(receiverData.AssetID) + if err != nil { + log.Printf("Failed go get transaction receiver asset: %s", err) + err = common.StringError(err) + return + } + + amount, err := common.BigNumberToFloat(senderData.Value, 6) + if err != nil { + log.Printf("Failed to convert amount: %s", err) + err = common.StringError(err) + return + } + + senderAmount, err := common.BigNumberToFloat(senderData.Amount, senderAsset.Decimals) + if err != nil { + log.Printf("Failed to convert senderAmount: %s", err) + err = common.StringError(err) + return + } + + receiverAmount, err := common.BigNumberToFloat(receiverData.Amount, receiverAsset.Decimals) + if err != nil { + log.Printf("Failed to convert receiverAmount: %s", err) + err = common.StringError(err) + return + } + + stringFee, err := common.BigNumberToFloat(transaction.StringFee, 6) + if err != nil { + log.Printf("Failed to convert stringFee: %s", err) + err = common.StringError(err) + return + } + + processingFee, err := common.BigNumberToFloat(transaction.ProcessingFee, 6) + if err != nil { + log.Printf("Failed to convert processingFee: %s", err) + err = common.StringError(err) + return + } + + txData = transactionData{ + Amount: amount, + SentAmount: senderAmount, + SentCurrency: senderAsset.Name, + SenderEntityId: senderData.UserID, + SenderEntityType: senderType, + SenderInstrumentId: senderData.InstrumentID, + ReceivedAmount: receiverAmount, + ReceivedCurrency: receiverAsset.Name, + ReceiverEntityId: receiverData.UserID, + ReceiverEntityType: receiverType, + ReceiverInstrumentId: receiverData.InstrumentID, + ExchangeRate: senderAmount / receiverAmount, + TransactionHash: transaction.TransactionHash, + USDConversionNotes: "", + InternalFee: stringFee, + ExternalFee: processingFee, + } + + return +} + +func mapToUnit21Event(transaction model.Transaction, transactionData transactionData) *u21Event { + var transactionTagArr []string + if transaction.Tags != nil { + for key, value := range transaction.Tags { + transactionTagArr = append(transactionTagArr, key+":"+value) + } + } + + jsonBody := &u21Event{ + GeneralData: &eventGeneral{ + EventId: transaction.ID, + EventType: "transaction", + EventTime: int(transaction.CreatedAt.Unix()), + EventSubtype: "", + Status: transaction.Status, + Parents: nil, + Tags: transactionTagArr, + }, + TransactionData: &transactionData, + ActionData: nil, + DigitalData: &eventDigitalData{ + IPAddress: transaction.IPAddress, + }, + LocationData: nil, + CustomData: nil, + } + + return jsonBody +} diff --git a/pkg/internal/unit21/transaction_test.go b/pkg/internal/unit21/transaction_test.go new file mode 100644 index 00000000..deaceda4 --- /dev/null +++ b/pkg/internal/unit21/transaction_test.go @@ -0,0 +1,162 @@ +package unit21 + +import ( + "log" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/joho/godotenv" + "github.com/lib/pq" + "github.com/stretchr/testify/assert" +) + +func TestCreateTransaction(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + transactionId := uuid.NewString() + db, mock, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + transaction := model.Transaction{ + ID: transactionId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Type: "fiat-to-crypto", + Status: "Completed", + Tags: map[string]string{}, + DeviceID: uuid.NewString(), + IPAddress: "192.0.1.1", + PlatformID: uuid.NewString(), + TransactionHash: "", + NetworkID: uuid.NewString(), + NetworkFee: "100000000", + ContractParams: pq.StringArray{}, + ContractFunc: "mintTo()", + TransactionAmount: "1000000000", + OriginTxLegID: uuid.NewString(), + ReceiptTxLegID: uuid.NewString(), + ResponseTxLegID: uuid.NewString(), + DestinationTxLegID: uuid.NewString(), + ProcessingFee: "1000000", + ProcessingFeeAsset: uuid.NewString(), + StringFee: "1000000", + } + + mockedTxLegRow1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), "1000000", "1000000", uuid.NewString(), uuid.NewString(), uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM tx_leg WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedTxLegRow1) + + mockedTxLegRow2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), "10000000", "10000000", uuid.NewString(), uuid.NewString(), uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM tx_leg WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedTxLegRow2) + + mockedUserRow1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "deactivated_at", "type", "status", "tags", "first_name", "middle_name", "last_name"}).AddRow(uuid.NewString(), time.Now(), time.Now(), nil, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") + mock.ExpectQuery(`SELECT \* FROM user WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedUserRow1) + + mockedUserRow2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "deactivated_at", "type", "status", "tags", "first_name", "middle_name", "last_name"}).AddRow(uuid.NewString(), time.Now(), time.Now(), nil, "User", "Onboarded", `{"kyc_level": "1", "platform": "space invaders"}`, "Toph", "", "Bei Fong") + mock.ExpectQuery(`SELECT \* FROM user WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedUserRow2) + + mockedAssetRow1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}).AddRow(uuid.NewString(), time.Now(), time.Now(), "USD", "fiat USD", 6, false, uuid.NewString(), "self") + mock.ExpectQuery(`SELECT \* FROM asset WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedAssetRow1) + + mockedAssetRow2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}).AddRow(uuid.NewString(), time.Now(), time.Now(), "Noose The Goose", "Noose the Goose NFT", 1, true, uuid.NewString(), "joepegs.com") + mock.ExpectQuery(`SELECT \* FROM asset WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedAssetRow2) + + repo := TransactionRepo{ + txLeg: repository.NewTxLeg((sqlxDB)), + user: repository.NewUser(sqlxDB), + asset: repository.NewAsset(sqlxDB), + } + + u21Transaction := NewTransaction(repo) + + u21TransactionId, err := u21Transaction.Create(transaction) + assert.NoError(t, err) + log.Printf("u21TransactionId: %s", u21TransactionId) + assert.Greater(t, len([]rune(u21TransactionId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new transaction added + // todo: mock call to client once it's manually tested +} + +func TestUpdateTransaction(t *testing.T) { + err := godotenv.Load("../../../.env") + assert.NoError(t, err) + + transactionId := "866c32ae-9fda-409c-a0be-28b830022e93" + db, mock, err := sqlmock.New() + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + defer db.Close() + + transaction := model.Transaction{ + ID: transactionId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Type: "fiat-to-crypto", + Status: "Completed", + Tags: map[string]string{}, + DeviceID: uuid.NewString(), + IPAddress: "192.206.151.131", + PlatformID: uuid.NewString(), + TransactionHash: "", + NetworkID: uuid.NewString(), + NetworkFee: "100000000", + ContractParams: pq.StringArray{}, + ContractFunc: "mintTo()", + TransactionAmount: "1000000000", + OriginTxLegID: uuid.NewString(), + ReceiptTxLegID: uuid.NewString(), + ResponseTxLegID: uuid.NewString(), + DestinationTxLegID: uuid.NewString(), + ProcessingFee: "1000000", + ProcessingFeeAsset: uuid.NewString(), + StringFee: "2000000", + } + + mockedTxLegRow1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), "1000000", "1000000", uuid.NewString(), uuid.NewString(), uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM tx_leg WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedTxLegRow1) + + mockedTxLegRow2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}).AddRow(uuid.NewString(), time.Now(), time.Now(), time.Now(), "10000000", "10000000", uuid.NewString(), uuid.NewString(), uuid.NewString()) + mock.ExpectQuery(`SELECT \* FROM tx_leg WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedTxLegRow2) + + mockedUserRow1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "deactivated_at", "type", "status", "tags", "first_name", "middle_name", "last_name"}).AddRow(uuid.NewString(), time.Now(), time.Now(), nil, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") + mock.ExpectQuery(`SELECT \* FROM user WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedUserRow1) + + mockedUserRow2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "deactivated_at", "type", "status", "tags", "first_name", "middle_name", "last_name"}).AddRow(uuid.NewString(), time.Now(), time.Now(), nil, "User", "Onboarded", `{"kyc_level": "1", "platform": "space invaders"}`, "Toph", "", "Bei Fong") + mock.ExpectQuery(`SELECT \* FROM user WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedUserRow2) + + mockedAssetRow1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}).AddRow(uuid.NewString(), time.Now(), time.Now(), "USD", "fiat USD", 6, false, uuid.NewString(), "self") + mock.ExpectQuery(`SELECT \* FROM asset WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedAssetRow1) + + mockedAssetRow2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}).AddRow(uuid.NewString(), time.Now(), time.Now(), "Noose The Goose", "Noose the Goose NFT", 1, true, uuid.NewString(), "joepegs.com") + mock.ExpectQuery(`SELECT \* FROM asset WHERE id = (.+) AND 'deactivated_at' IS NOT NULL`).WithArgs().WillReturnRows(mockedAssetRow2) + + repo := TransactionRepo{ + txLeg: repository.NewTxLeg((sqlxDB)), + user: repository.NewUser(sqlxDB), + asset: repository.NewAsset(sqlxDB), + } + + u21Transaction := NewTransaction(repo) + + u21TransactionId, err := u21Transaction.Update(transaction) + assert.NoError(t, err) + log.Printf("u21TransactionId: %s", u21TransactionId) + assert.Greater(t, len([]rune(u21TransactionId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new transaction added + // todo: mock call to client once it's manually tested +} diff --git a/pkg/internal/unit21/types.go b/pkg/internal/unit21/types.go new file mode 100644 index 00000000..5878d731 --- /dev/null +++ b/pkg/internal/unit21/types.go @@ -0,0 +1,196 @@ +package unit21 + +// ////////////////////////////////////////////////////////////////// +// Entity +type u21Entity struct { + GeneralData *entityGeneral `json:"general_data"` + UserData *entityPersonal `json:"user_data,omitempty"` + CommunicationData *entityCommunication `json:"communication_data,omitempty"` + DigitalData *entityDigitalData `json:"digital_data,omitempty"` + CustomData *entityCustomData `json:"custom_data,omitempty"` + WorkflowOptions *options `json:"options,omitempty"` +} + +type entityGeneral struct { + EntityId string `json:"entity_id"` + EntityType string `json:"entity_type"` //employee or business - says user in the spreadsheet? + EntitySubType string `json:"entity_subtype,omitempty"` + Status string `json:"status,omitempty"` + RegisteredAt int `json:"registered_at"` //date in seconds since 1/1/1970 + Tags []string `json:"tags,omitempty"` //list of format: keyString:valueString +} + +type entityPersonal struct { + FirstName string `json:"first_name"` + MiddleName string `json:"middle_name,omitempty"` + LastName string `json:"last_name"` + BirthDay int `json:"day_of_birth,omitempty"` + BirthMonth int `json:"month_of_birth,omitempty"` + BirthYear int `json:"year_of_birth,omitempty"` + SSN string `json:"ssn,omitempty"` +} + +type entityCommunication struct { + Emails []string `json:"email_addresses,omitempty"` + Phones []string `json:"phone_numbers,omitempty"` //E.164 format +12125551395 ( '[+][country code][area code][local phone number]' https://en.wikipedia.org/wiki/E.164 +} + +type entityDigitalData struct { + IpAddresses []string `json:"ip_addresses,omitempty"` //ipv4 or ipv6 + ClientFingerprints []string `json:"client_fingerprints,omitempty"` +} + +type entityCustomData struct { + //more can be added to this as needed + Platforms []string `json:"platforms,omitempty"` //where does this come from? do you want it called partnerName instead? +} + +type options struct { + ResolveGeoIp bool `json:"resolve_geoip"` //default true + MergeCustomData bool `json:"merge_custom_data"` //default false https://docs.unit21.ai/docs/how-data-merges-on-updates#custom-data-merge-strategy + UpsertOnConflict bool `json:"upsert_on_conflict"` //default true BUT should we change to false? don't need update endpoint if allowed +} + +type createEntityResponse struct { + Ignored bool `json:"ignored,omitempty"` + EntityId string `json:"entity_id"` + PreviouslyExisted bool `json:"previously_existed"` + Unit21Id string `json:"unit21_id"` +} + +type updateEntityResponse struct { + EntityId string `json:"entity_id"` + Unit21Id string `json:"unit21_id"` +} + +// ////////////////////////////////////////////////////////////////// +// Instrument +type u21Instrument struct { + InstrumentId string `json:"instrument_id"` + InstrumentType string `json:"instrument_type"` + InstrumentSubtype string `json:"instrument_subtype,omitempty"` + Source string `json:"source,omitempty"` + Status string `json:"status,omitempty"` + RegisteredAt int `json:"registered_at"` + ParentInstrumentId string `json:"parent_instrument_id,omitempty"` + Entities []instrumentEntity `json:"entities,omitempty"` + CustomData any `json:"custom_data,omitempty"` + DigitalData *instrumentDigitalData `json:"digital_data,omitempty"` + LocationData *instrumentLocationData `json:"location_data,omitempty"` + Tags []string `json:"tags,omitempty"` + Options *options `json:"options,omitempty"` +} + +type instrumentEntity struct { + EntityId string `json:"entity_id"` + RelationshipId string `json:"relationship_id,omitempty"` + EntityType string `json:"entity_type,omitempty"` +} + +type instrumentDigitalData struct { + IpAddresses []string `json:"ip_addresses,omitempty"` +} + +type instrumentLocationData struct { + Type string `json:"type"` + BuildingNumber string `json:"building_number"` + UnitNumber string `json:"unit_number,omitempty"` + StreetName string `json:"street_name"` + City string `json:"city"` + State string `json:"state"` + PostalCode string `json:"postal_code"` + Country string `json:"country"` + VerifiedOn int `json:"verified_on"` +} + +type instrumentCustomData struct { + //more can be added to this as needed + None any `json:"none"` +} + +type createInstrumentResponse struct { + Ignored bool `json:"ignored,omitempty"` + InstrumentId string `json:"instrument_id"` + PreviouslyExisted bool `json:"previously_existed"` + Unit21Id string `json:"unit21_id"` +} + +type updateInstrumentResponse struct { + InstrumentId string `json:"instrument_id"` + Unit21Id string `json:"unit21_id"` +} + +// ////////////////////////////////////////////////////////////////// +// Event + +type u21Event struct { + GeneralData *eventGeneral `json:"general_data"` + TransactionData *transactionData `json:"transaction_data"` + ActionData *actionData `json:"action_data,omitempty"` + DigitalData *eventDigitalData `json:"digital_data,omitempty"` + LocationData *instrumentLocationData `json:"location_data,omitempty"` + CustomData *eventCustomData `json:"custom_data,omitempty"` +} + +type eventGeneral struct { + EventId string `json:"event_id"` + EventType string `json:"event_type"` + EventTime int `json:"event_time"` + EventSubtype string `json:"event_subtype,omitempty"` + Status string `json:"status,omitempty"` + Parents *eventParent `json:"parents,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +type transactionData struct { + Amount float64 `json:"amount"` + SentAmount float64 `json:"sent_amount,omitempty"` + SentCurrency string `json:"sent_currency,omitempty"` + SenderEntityId string `json:"sender_entity_id,omitempty"` + SenderEntityType string `json:"sender_entity_type,omitempty"` + SenderInstrumentId string `json:"sender_instrument_id"` + ReceivedAmount float64 `json:"received_amount,omitempty"` + ReceivedCurrency string `json:"received_currency,omitempty"` + ReceiverEntityId string `json:"receiver_entity_id,omitempty"` + ReceiverEntityType string `json:"receiver_entity_type,omitempty"` + ReceiverInstrumentId string `json:"receiver_instrument_id"` + ExchangeRate float64 `json:"exchange_rate,omitempty"` + TransactionHash string `json:"transaction_hash,omitempty"` + USDConversionNotes string `json:"usd_conversion_notes,omitempty"` + InternalFee float64 `json:"internal_fee,omitempty"` + ExternalFee float64 `json:"external_fee,omitempty"` +} + +type actionData struct { + ActionType string `json:"action_type,omitempty"` + ActionDetails string `json:"action_details,omitempty"` + EntityId string `json:"entity_id"` + EntityType string `json:"entity_type"` + InstrumentId string `json:"instrument_id,omitempty"` +} + +type eventDigitalData struct { + IPAddress string `json:"ip_address,omitempty"` +} + +type eventCustomData struct { + //more can be added to this as needed + None any `json:"none"` +} + +type eventParent struct { + EventId string `json:"event_id"` + EventType string `json:"event_type"` +} + +type createEventResponse struct { + Ignored bool `json:"ignored,omitempty"` + EventId string `json:"event_id"` + PreviouslyExisted bool `json:"previously_existed"` + Unit21Id string `json:"unit21_id"` +} + +type updateEventResponse struct { + EventId string `json:"event_id"` + Unit21Id string `json:"unit21_id"` +} diff --git a/pkg/model/custom.go b/pkg/model/custom.go index e2a4326c..d18e5dba 100644 --- a/pkg/model/custom.go +++ b/pkg/model/custom.go @@ -4,12 +4,9 @@ import ( "database/sql/driver" "encoding/json" "errors" - - "github.com/lib/pq" ) type StringMap map[string]string -type StringArray pq.StringArray func (sm StringMap) Value() (driver.Value, error) { return json.Marshal(sm) diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 37241b36..592b47ca 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -4,6 +4,8 @@ import ( "database/sql" "encoding/json" "time" + + "github.com/lib/pq" ) // See STRING_USER in Migrations 0001 @@ -11,7 +13,7 @@ type User struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` Type string `json:"type" db:"type"` Status string `json:"status" db:"status"` Tags StringMap `json:"tags" db:"tags"` @@ -25,7 +27,7 @@ type Platform struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` Type string `json:"type" db:"type"` Status string `json:"status" db:"status"` Name string `json:"name" db:"name"` @@ -59,19 +61,25 @@ type Asset struct { ValueOracle sql.NullString `json:"valueOracle" db:"value_oracle"` } +// See USER_PLATFORM in Migrations 0002 +type UserPlatform struct { + UserID string `json:"userId" db:"user_id"` + PlatformID string `json:"platformId" db:"platform_id"` +} + // See DEVICE in Migrations 0002 type Device struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - LastUsedAt time.Time `json:"lastUsedAt" db:"last_used_at"` - ValidatedAt time.Time `json:"validatedAt" db:"validated_at"` - DeactivatedAt time.Time `json:"deactivatedAt" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Description string `json:"description" db:"description"` - Fingerprint string `json:"fingerprint" db:"fingerprint"` - IpAddresses StringArray `json:"ipAddresses" db:"ip_addresses"` - UserID string `json:"userId" db:"user_id"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + LastUsedAt time.Time `json:"lastUsedAt" db:"last_used_at"` + ValidatedAt time.Time `json:"validatedAt" db:"validated_at"` + DeactivatedAt time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + Type string `json:"type" db:"type"` + Description string `json:"description" db:"description"` + Fingerprint string `json:"fingerprint" db:"fingerprint"` + IpAddresses pq.StringArray `json:"ipAddresses" db:"ip_addresses"` + UserID string `json:"userId" db:"user_id"` } // See CONTACT in Migrations 0002 @@ -81,7 +89,7 @@ type Contact struct { CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` LastAuthenticatedAt *time.Time `json:"lastAuthenticatedAt" db:"last_authenticated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` Type string `json:"type" db:"type"` Status string `json:"status" db:"status"` Data string `json:"data" db:"data"` @@ -110,7 +118,7 @@ type Instrument struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` Type string `json:"type" db:"type"` Status string `json:"status" db:"status"` Tags StringMap `json:"tags" db:"tags"` @@ -123,25 +131,17 @@ type Instrument struct { // See CONTACT_PLATFORM in Migrations 0003 type ContactPlatform struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` - ContactID string `json:"contactId" db:"contact_id"` - PlatformID string `json:"platformId" db:"platform_id"` + ContactID string `json:"contactId" db:"contact_id"` + PlatformID string `json:"platformId" db:"platform_id"` } // See DEVICE_INSTRUMENT in Migrations 0003 type DeviceInstrument struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` - DeviceID string `json:"deviceId" db:"device_id"` - InstrumentID string `json:"instrumentId" db:"instrument_id"` + DeviceID string `json:"deviceId" db:"device_id"` + InstrumentID string `json:"instrumentId" db:"instrument_id"` } -// See TX_LEG in Migrations 0003 +// See Tx_LEG in Migrations 0003 type TxLeg struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` @@ -156,28 +156,28 @@ type TxLeg struct { // See TRANSACTION in Migrations 0003 type Transaction struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags StringMap `json:"tags" db:"tags"` // TODO: Fix this alongside Unit21 integration - DeviceID string `json:"deviceId" db:"device_id"` - IPAddress string `json:"ipAddress" db:"ip_address"` - PlatformID string `json:"platformId" db:"platform_id"` - TransactionHash string `json:"transactionHash" db:"transaction_hash"` - NetworkID string `json:"networkId" db:"network_id"` - NetworkFee string `json:"networkFee" db:"network_fee"` - ContractParams StringArray `json:"contractParameters" db:"contract_params"` - ContractFunc string `json:"contractFunc" db:"contract_func"` - TransactionAmount string `json:"transactionAmount" db:"transaction_amount"` - OriginTXLegID string `json:"originTXLegId" db:"origin_tx_leg_id"` - ReceiptTXLegID string `json:"receiptTXLegId" db:"receipt_tx_leg_id"` - ResponseTXLegID string `json:"responseTXLegId" db:"response_tx_leg_id"` - DestinationTXLegID string `json:"destinationTXLegId" db:"destination_tx_leg_id"` - ProcessingFee string `json:"processingFee" db:"processing_fee"` - ProcessingFeeAsset string `json:"processingFeeAsset" db:"processing_fee_asset"` - StringFee string `json:"stringFee" db:"string_fee"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags StringMap `json:"tags" db:"tags"` + DeviceID string `json:"deviceId" db:"device_id"` + IPAddress string `json:"ipAddress" db:"ip_address"` + PlatformID string `json:"platformId" db:"platform_id"` + TransactionHash string `json:"transactionHash" db:"transaction_hash"` + NetworkID string `json:"networkId" db:"network_id"` + NetworkFee string `json:"networkFee" db:"network_fee"` + ContractParams pq.StringArray `json:"contractParameters" db:"contract_params"` + ContractFunc string `json:"contractFunc" db:"contract_func"` + TransactionAmount string `json:"transactionAmount" db:"transaction_amount"` + OriginTxLegID string `json:"originTxLegId" db:"origin_tx_leg_id"` + ReceiptTxLegID string `json:"receiptTxLegId" db:"receipt_tx_leg_id"` + ResponseTxLegID string `json:"responseTxLegId" db:"response_tx_leg_id"` + DestinationTxLegID string `json:"destinationTxLegId" db:"destination_tx_leg_id"` + ProcessingFee string `json:"processingFee" db:"processing_fee"` + ProcessingFeeAsset string `json:"processingFeeAsset" db:"processing_fee_asset"` + StringFee string `json:"stringFee" db:"string_fee"` } type AuthStrategy struct { diff --git a/pkg/model/request.go b/pkg/model/request.go index c2b5b467..d0835749 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -19,10 +19,10 @@ type TransactionUpdates struct { ContractParams *types.JSONText `json:"contractParameters" db:"contract_params"` ContractFunc *string `json:"contractFunc" db:"contract_func"` TransactionAmount *string `json:"transactionAmount" db:"transaction_amount"` - OriginTXLegID *string `json:"originTXLegId" db:"origin_tx_leg_id"` - ReceiptTXLegID *string `json:"receiptTXLegId" db:"receipt_tx_leg_id"` - ResponseTXLegID *string `json:"responseTXLegId" db:"response_tx_leg_id"` - DestinationTXLegID *string `json:"destinationTXLegId" db:"destination_tx_leg_id"` + OriginTxLegID *string `json:"originTxLegId" db:"origin_tx_leg_id"` + ReceiptTxLegID *string `json:"receiptTxLegId" db:"receipt_tx_leg_id"` + ResponseTxLegID *string `json:"responseTxLegId" db:"response_tx_leg_id"` + DestinationTxLegID *string `json:"destinationTxLegId" db:"destination_tx_leg_id"` ProcessingFee *string `json:"processingFee" db:"processing_fee"` ProcessingFeeAsset *string `json:"processingFeeAsset" db:"processing_fee_asset"` StringFee *string `json:"stringFee" db:"string_fee"` @@ -56,7 +56,7 @@ type UserUpdates struct { LastName *string `json:"lastName" db:"last_name"` } -type UserContactUpdates struct { +type ContactUpdates struct { DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` Type *string `json:"type" db:"type"` Status *string `json:"status" db:"status"` diff --git a/pkg/repository/asset.go b/pkg/repository/asset.go index 4b029a93..78d88845 100644 --- a/pkg/repository/asset.go +++ b/pkg/repository/asset.go @@ -12,7 +12,7 @@ import ( type Asset interface { Transactable Create(model.Asset) (model.Asset, error) - GetID(id string) (model.Asset, error) + GetById(id string) (model.Asset, error) GetName(name string) (model.Asset, error) Update(ID string, updates any) error } diff --git a/pkg/repository/base.go b/pkg/repository/base.go index 32970d1c..9323a9a2 100644 --- a/pkg/repository/base.go +++ b/pkg/repository/base.go @@ -52,7 +52,7 @@ type Transactable interface { Rollback() // Commit commits the undelying Tx and resets to back to *sqlx.DB from *sqlx.Tx Commit() error - // SetTx sets the underying store to be sqlx.TX so it can be used for transaction across multiple repos + // SetTx sets the underying store to be sqlx.Tx so it can be used for transaction across multiple repos SetTx(t Queryable) // Reset changes the store back to *sqlx.DB from *sqlx.Tx // Useful when there are many repos using the same *sqlx.Tx @@ -112,7 +112,7 @@ func (b base[T]) List(limit int, offset int) (list []T, err error) { return list, err } -func (b base[T]) GetID(ID string) (m T, err error) { +func (b base[T]) GetById(ID string) (m T, err error) { err = b.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE id = $1 AND 'deactivated_at' IS NOT NULL", b.table), ID) if err != nil && err == sql.ErrNoRows { return m, common.StringError(ErrNotFound) @@ -121,7 +121,7 @@ func (b base[T]) GetID(ID string) (m T, err error) { } // Returns the first match of the user's ID -func (b base[T]) GetUserID(userID string) (m T, err error) { +func (b base[T]) GetByUserId(userID string) (m T, err error) { err = b.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE user_id = $1 AND 'deactivated_at' IS NOT NULL LIMIT 1", b.table), userID) if err != nil && err == sql.ErrNoRows { return m, common.StringError(ErrNotFound) @@ -129,7 +129,7 @@ func (b base[T]) GetUserID(userID string) (m T, err error) { return m, nil } -func (b base[T]) ListUserID(userID string, limit int, offset int) ([]T, error) { +func (b base[T]) ListByUserId(userID string, limit int, offset int) ([]T, error) { list := []T{} if limit == 0 { limit = 20 diff --git a/pkg/repository/base_test.go b/pkg/repository/base_test.go index 0aa950ec..e099d8c9 100644 --- a/pkg/repository/base_test.go +++ b/pkg/repository/base_test.go @@ -17,9 +17,9 @@ func TestBaseUpdate(t *testing.T) { defer db.Close() mock.ExpectExec(`UPDATE contact SET`).WithArgs("type") mType := "type" - m := model.UserContactUpdates{Type: &mType} + m := model.ContactUpdates{Type: &mType} - NewUserContact(sqlxDB).Update("ID", m) + NewContact(sqlxDB).Update("ID", m) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("error '%s' was not expected, while updating a contact", err) } diff --git a/pkg/repository/contact.go b/pkg/repository/contact.go index 0fb0347d..4889da0e 100644 --- a/pkg/repository/contact.go +++ b/pkg/repository/contact.go @@ -10,7 +10,10 @@ type Contact interface { Transactable Readable Create(model.Contact) (model.Contact, error) - GetID(ID string) (model.Contact, error) + GetById(ID string) (model.Contact, error) + GetByUserId(userID string) (model.Contact, error) + ListByUserId(userID string, imit int, offset int) ([]model.Contact, error) + List(limit int, offset int) ([]model.Contact, error) Update(ID string, updates any) error } @@ -19,14 +22,14 @@ type contact[T any] struct { } func NewContact(db *sqlx.DB) Contact { - return &contact[model.Contact]{base[model.Contact]{store: db, table: "contact"}} + return &contact[model.Contact]{base: base[model.Contact]{store: db, table: "contact"}} } -func (c contact[T]) Create(insert model.Contact) (model.Contact, error) { +func (u contact[T]) Create(insert model.Contact) (model.Contact, error) { m := model.Contact{} - rows, err := c.store.NamedQuery(` - INSERT INTO contact (type, user_id, status) - VALUES(:type, :user_id, :status) RETURNING *`, insert) + rows, err := u.store.NamedQuery(` + INSERT INTO contact (user_id, data, type, status) + VALUES(:user_id, :data, :type, :status) RETURNING *`, insert) if err != nil { return m, common.StringError(err) } diff --git a/pkg/repository/contact_platform.go b/pkg/repository/contact_platform.go new file mode 100644 index 00000000..8f4a29d0 --- /dev/null +++ b/pkg/repository/contact_platform.go @@ -0,0 +1,42 @@ +package repository + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/jmoiron/sqlx" +) + +type ContactPlatform interface { + Transactable + Readable + Create(model.ContactPlatform) (model.ContactPlatform, error) + GetById(ID string) (model.ContactPlatform, error) + List(limit int, offset int) ([]model.ContactPlatform, error) + Update(ID string, updates any) error +} + +type contactPlatform[T any] struct { + base[T] +} + +func NewContactPlatform(db *sqlx.DB) ContactPlatform { + return &contactPlatform[model.ContactPlatform]{base: base[model.ContactPlatform]{store: db, table: "contact_platform"}} +} + +func (u contactPlatform[T]) Create(insert model.ContactPlatform) (model.ContactPlatform, error) { + m := model.ContactPlatform{} + rows, err := u.store.NamedQuery(` + INSERT INTO contact_platform (contact_id, platform_id) + VALUES(:contact_id, :platform_id) RETURNING *`, insert) + if err != nil { + return m, common.StringError(err) + } + for rows.Next() { + err = rows.StructScan(&m) + if err != nil { + return m, common.StringError(err) + } + } + defer rows.Close() + return m, nil +} diff --git a/pkg/repository/device.go b/pkg/repository/device.go index eaac2a3d..7adf1244 100644 --- a/pkg/repository/device.go +++ b/pkg/repository/device.go @@ -9,7 +9,9 @@ import ( type Device interface { Transactable Create(model.Device) (model.Device, error) - GetID(id string) (model.Device, error) + GetById(id string) (model.Device, error) + GetByUserId(userID string) (model.Device, error) + ListByUserId(userID string, imit int, offset int) ([]model.Device, error) Update(ID string, updates any) error } diff --git a/pkg/repository/instrument.go b/pkg/repository/instrument.go index 80a0cc80..50636647 100644 --- a/pkg/repository/instrument.go +++ b/pkg/repository/instrument.go @@ -12,7 +12,7 @@ import ( type Instrument interface { Transactable Create(model.Instrument) (model.Instrument, error) - GetID(id string) (model.Instrument, error) + GetById(id string) (model.Instrument, error) GetWallet(addr string) (model.Instrument, error) Update(ID string, updates any) error } diff --git a/pkg/repository/location.go b/pkg/repository/location.go new file mode 100644 index 00000000..737b908e --- /dev/null +++ b/pkg/repository/location.go @@ -0,0 +1,41 @@ +package repository + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/jmoiron/sqlx" +) + +type Location interface { + Transactable + Create(model.Location) (model.Location, error) + GetById(id string) (model.Location, error) + Update(ID string, updates any) error +} + +type location[T any] struct { + base[T] +} + +func NewLocation(db *sqlx.DB) Location { + return &location[model.Location]{base[model.Location]{store: db, table: "location"}} +} + +func (i location[T]) Create(insert model.Location) (model.Location, error) { + m := model.Location{} + rows, err := i.store.NamedQuery(` + INSERT INTO location (name) + VALUES(:name) RETURNING *`, insert) + if err != nil { + return m, common.StringError(err) + } + for rows.Next() { + err = rows.StructScan(&m) + if err != nil { + return m, common.StringError(err) + } + } + + defer rows.Close() + return m, nil +} diff --git a/pkg/repository/network.go b/pkg/repository/network.go index d2abc996..faffadec 100644 --- a/pkg/repository/network.go +++ b/pkg/repository/network.go @@ -12,7 +12,7 @@ import ( type Network interface { Transactable Create(model.Network) (model.Network, error) - GetID(id string) (model.Network, error) + GetById(id string) (model.Network, error) GetChainID(chainId uint64) (model.Network, error) Update(ID string, updates any) error } diff --git a/pkg/repository/platform.go b/pkg/repository/platform.go index 55b49d95..e1ff5715 100644 --- a/pkg/repository/platform.go +++ b/pkg/repository/platform.go @@ -19,7 +19,7 @@ type PlaformUpdates struct { type Platform interface { Transactable Create(model.Platform) (model.Platform, error) - GetID(ID string) (model.Platform, error) + GetById(ID string) (model.Platform, error) List(limit int, offset int) ([]model.Platform, error) Update(ID string, updates any) error } diff --git a/pkg/repository/transaction.go b/pkg/repository/transaction.go index c14e29cb..80f1cf69 100644 --- a/pkg/repository/transaction.go +++ b/pkg/repository/transaction.go @@ -9,7 +9,7 @@ import ( type Transaction interface { Transactable Create(model.Transaction) (model.Transaction, error) - GetID(id string) (model.Transaction, error) + GetById(id string) (model.Transaction, error) Update(ID string, updates any) error } diff --git a/pkg/repository/tx_leg.go b/pkg/repository/tx_leg.go index 6be512c2..f3657e53 100644 --- a/pkg/repository/tx_leg.go +++ b/pkg/repository/tx_leg.go @@ -9,7 +9,7 @@ import ( type TxLeg interface { Transactable Create(model.TxLeg) (model.TxLeg, error) - GetID(id string) (model.TxLeg, error) + GetById(id string) (model.TxLeg, error) Update(ID string, updates any) error } diff --git a/pkg/repository/user.go b/pkg/repository/user.go index a0c714b7..f88bc0b0 100644 --- a/pkg/repository/user.go +++ b/pkg/repository/user.go @@ -10,7 +10,7 @@ type User interface { Transactable Readable Create(model.User) (model.User, error) - GetID(ID string) (model.User, error) + GetById(ID string) (model.User, error) List(limit int, offset int) ([]model.User, error) Update(ID string, updates any) error } diff --git a/pkg/repository/user_contact.go b/pkg/repository/user_contact.go deleted file mode 100644 index 0b3ff239..00000000 --- a/pkg/repository/user_contact.go +++ /dev/null @@ -1,44 +0,0 @@ -package repository - -import ( - "github.com/String-xyz/string-api/pkg/internal/common" - "github.com/String-xyz/string-api/pkg/model" - "github.com/jmoiron/sqlx" -) - -type UserContact interface { - Transactable - Readable - Create(model.Contact) (model.Contact, error) - GetID(ID string) (model.Contact, error) - GetUserID(userID string) (model.Contact, error) - ListUserID(userID string, imit int, offset int) ([]model.Contact, error) - List(limit int, offset int) ([]model.Contact, error) - Update(ID string, updates any) error -} - -type userContact[T any] struct { - base[T] -} - -func NewUserContact(db *sqlx.DB) UserContact { - return &userContact[model.Contact]{base: base[model.Contact]{store: db, table: "contact"}} -} - -func (u userContact[T]) Create(insert model.Contact) (model.Contact, error) { - m := model.Contact{} - rows, err := u.store.NamedQuery(` - INSERT INTO contact (user_id, data, type, status) - VALUES(:user_id, :data, :type, :status) RETURNING *`, insert) - if err != nil { - return m, common.StringError(err) - } - for rows.Next() { - err = rows.StructScan(&m) - if err != nil { - return m, common.StringError(err) - } - } - defer rows.Close() - return m, nil -} diff --git a/pkg/repository/user_platform.go b/pkg/repository/user_platform.go new file mode 100644 index 00000000..b098150d --- /dev/null +++ b/pkg/repository/user_platform.go @@ -0,0 +1,43 @@ +package repository + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/jmoiron/sqlx" +) + +type UserPlatform interface { + Transactable + Readable + Create(model.UserPlatform) (model.UserPlatform, error) + GetById(ID string) (model.UserPlatform, error) + List(limit int, offset int) ([]model.UserPlatform, error) + ListByUserId(userID string, imit int, offset int) ([]model.UserPlatform, error) + Update(ID string, updates any) error +} + +type userPlatform[T any] struct { + base[T] +} + +func NewUserPlatform(db *sqlx.DB) UserPlatform { + return &userPlatform[model.UserPlatform]{base: base[model.UserPlatform]{store: db, table: "user_platform"}} +} + +func (u userPlatform[T]) Create(insert model.UserPlatform) (model.UserPlatform, error) { + m := model.UserPlatform{} + rows, err := u.store.NamedQuery(` + INSERT INTO user_platform (user_id, platform_id) + VALUES(:user_id, :platform_id) RETURNING *`, insert) + if err != nil { + return m, common.StringError(err) + } + for rows.Next() { + err = rows.StructScan(&m) + if err != nil { + return m, common.StringError(err) + } + } + defer rows.Close() + return m, nil +} diff --git a/pkg/repository/user_test.go b/pkg/repository/user_test.go index 2041867c..b84e53a5 100644 --- a/pkg/repository/user_test.go +++ b/pkg/repository/user_test.go @@ -45,7 +45,7 @@ func TestGetUser(t *testing.T) { mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WillReturnRows(rows).WithArgs(id) - NewUser(sqlxDB).GetID(id) + NewUser(sqlxDB).GetById(id) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("error '%s' was not expected, getting user by id", err) } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 1d66cb4e..8abbe20c 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -53,10 +53,10 @@ type Auth interface { type auth struct { authRepo repository.AuthStrategy userRepo repository.User - contactRepo repository.UserContact + contactRepo repository.Contact } -func NewAuth(a repository.AuthStrategy, u repository.User, c repository.UserContact) Auth { +func NewAuth(a repository.AuthStrategy, u repository.User, c repository.Contact) Auth { return &auth{a, u, c} } diff --git a/pkg/service/auth_test.go b/pkg/service/auth_test.go index 58db543e..ec1269bf 100644 --- a/pkg/service/auth_test.go +++ b/pkg/service/auth_test.go @@ -11,14 +11,14 @@ import ( ) func TestGenerateJWT(t *testing.T) { - a := NewAuth(stubs.AuthStrategyRepo{}, repository.NewUser(mocks.MockedDB()), repository.NewUserContact(mocks.MockedDB())) + a := NewAuth(stubs.AuthStrategyRepo{}, repository.NewUser(mocks.MockedDB()), repository.NewContact(mocks.MockedDB())) m := model.User{ID: "id"} token, err := a.GenerateJWT(m) assert.NoError(t, err) assert.NotEmpty(t, token.Token) } func TestValidate(t *testing.T) { - a := NewAuth(stubs.AuthStrategyRepo{}, repository.NewUser(mocks.MockedDB()), repository.NewUserContact(mocks.MockedDB())) + a := NewAuth(stubs.AuthStrategyRepo{}, repository.NewUser(mocks.MockedDB()), repository.NewContact(mocks.MockedDB())) m := model.User{ID: "id"} token, err := a.GenerateJWT(m) assert.NoError(t, err) diff --git a/pkg/service/chain.go b/pkg/service/chain.go index f9b6f5b3..bca9004d 100644 --- a/pkg/service/chain.go +++ b/pkg/service/chain.go @@ -24,7 +24,7 @@ func ChainInfo(chainId uint64, networkRepo repository.Network, assetRepo reposit if err != nil { return Chain{}, common.StringError(err) } - asset, err := assetRepo.GetID(network.GasTokenID) + asset, err := assetRepo.GetById(network.GasTokenID) if err != nil { return Chain{}, common.StringError(err) } diff --git a/pkg/service/platform.go b/pkg/service/platform.go index 30dcf030..e153565e 100644 --- a/pkg/service/platform.go +++ b/pkg/service/platform.go @@ -15,11 +15,11 @@ type Platform interface { type platform struct { platRepo repository.Platform - contactRepo repository.UserContact + contactRepo repository.Contact authRepo repository.AuthStrategy } -func NewPlatform(p repository.Platform, c repository.UserContact, a repository.AuthStrategy) Platform { +func NewPlatform(p repository.Platform, c repository.Contact, a repository.AuthStrategy) Platform { return &platform{p, c, a} } diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 8b52d819..0cf5aef4 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -81,7 +81,7 @@ func (t transaction) Execute(e model.ExecutionRequest) (model.TransactionReceipt return res, common.StringError(err) } - // Create new TX in repository, populate it with known info + // Create new Tx in repository, populate it with known info db, err := t.repos.Transaction.Create(model.Transaction{Status: "Created", NetworkID: chain.UUID}) if err != nil { return res, common.StringError(err) @@ -110,7 +110,7 @@ func (t transaction) Execute(e model.ExecutionRequest) (model.TransactionReceipt return res, common.StringError(err) } - // Test the TX and update model status + // Test the Tx and update model status estimateUSD, estimateETH, err := testTransaction(executor, e.TransactionRequest, chain, false) if err != nil { return res, common.StringError(err) @@ -175,7 +175,7 @@ func (t transaction) Execute(e model.ExecutionRequest) (model.TransactionReceipt // this Executor will not exist in scope of postProcess executor.Close() - // Send required information to new thread and return TXID to the endpoint + // Send required information to new thread and return TxID to the endpoint post := postProcessRequest{ TxID: txID, Chain: chain, @@ -226,7 +226,7 @@ func testTransaction(executor Executor, t model.TransactionRequest, chain Chain, TxValue: t.TxValue, TxGasLimit: t.TxGasLimit, } - // Estimate value and gas of TX request + // Estimate value and gas of Tx request estimateEVM, err := executor.Estimate(call) if err != nil { return res, 0, common.StringError(err) @@ -251,7 +251,7 @@ func testTransaction(executor Executor, t model.TransactionRequest, chain Chain, CostToken: *big.NewInt(0), TokenName: "", } - // Estimate Cost in USD to execute TX request + // Estimate Cost in USD to execute Tx request estimateUSD, err := cost.EstimateTransaction(estimationParams, chain) if err != nil { return res, eth, common.StringError(err) @@ -288,7 +288,7 @@ func (t transaction) authCard(userWallet string, cardToken string, usd float64, return "", common.StringError(err) } - // Create Origin TX leg + // Create Origin Tx leg usdWei := floatToFixedString(usd, int(chargeAsset.Decimals)) origin := model.TxLeg{ Timestamp: time.Now(), @@ -302,7 +302,7 @@ func (t transaction) authCard(userWallet string, cardToken string, usd float64, if err != nil { return auth, common.StringError(err) } - txLeg := model.TransactionUpdates{OriginTXLegID: &origin.ID} + txLeg := model.TransactionUpdates{OriginTxLegID: &origin.ID} err = t.repos.Transaction.Update(dbID, txLeg) if err != nil { return auth, common.StringError(err) @@ -324,7 +324,7 @@ func (t transaction) initiateTransaction(executor Executor, e model.ExecutionReq return "", nil, common.StringError(err) } - // Create Send TX leg + // Create Send Tx leg eth := common.WeiToEther(value) wei := floatToFixedString(eth, 18) usd := floatToFixedString(e.TotalUSD, int(chargeAsset.Decimals)) @@ -340,7 +340,7 @@ func (t transaction) initiateTransaction(executor Executor, e model.ExecutionReq if err != nil { return txID, value, common.StringError(err) } - txLeg := model.TransactionUpdates{ResponseTXLegID: &send.ID} + txLeg := model.TransactionUpdates{ResponseTxLegID: &send.ID} err = t.repos.Transaction.Update(txUUID, txLeg) if err != nil { return txID, value, common.StringError(err) @@ -349,7 +349,7 @@ func (t transaction) initiateTransaction(executor Executor, e model.ExecutionReq return txID, value, nil } -func confirmTX(executor Executor, txID string) (uint64, error) { +func confirmTx(executor Executor, txID string) (uint64, error) { trueGas, err := executor.TxWait(txID) if err != nil { return 0, common.StringError(err) @@ -363,7 +363,7 @@ func (t transaction) chargeCard(userWallet string, authorizationID string, usd f return common.StringError(err) } - // Create Receipt TX leg + // Create Receipt Tx leg usdWei := floatToFixedString(usd, int(chargeAsset.Decimals)) receipt := model.TxLeg{ Timestamp: time.Now(), @@ -377,7 +377,7 @@ func (t transaction) chargeCard(userWallet string, authorizationID string, usd f if err != nil { return common.StringError(err) } - txLeg := model.TransactionUpdates{ReceiptTXLegID: &receipt.ID} + txLeg := model.TransactionUpdates{ReceiptTxLegID: &receipt.ID} err = t.repos.Transaction.Update(txUUID, txLeg) if err != nil { return common.StringError(err) @@ -396,7 +396,7 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u } profit := quotedTotal - trueUSD - // Create Receive TX leg + // Create Receive Tx leg asset, err := t.repos.Asset.GetName("ETH") if err != nil { return profit, common.StringError(err) @@ -415,7 +415,7 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u if err != nil { return profit, common.StringError(err) } - txLeg := model.TransactionUpdates{DestinationTXLegID: &send.ID} + txLeg := model.TransactionUpdates{DestinationTxLegID: &send.ID} err = t.repos.Transaction.Update(txUUID, txLeg) if err != nil { return profit, common.StringError(err) @@ -451,12 +451,12 @@ func (t transaction) postProcess(request postProcessRequest) { // TODO: Handle error instead of returning it } - // confirm the TX on the EVM, update db status and NetworkFee - trueGas, err := confirmTX(executor, request.TxID) + // confirm the Tx on the EVM, update db status and NetworkFee + trueGas, err := confirmTx(executor, request.TxID) if err != nil { // TODO: Handle error instead of returning it } - status = "TX Confirmed" + status = "Tx Confirmed" updateDB.Status = &status networkFee := strconv.FormatUint(trueGas, 10) updateDB.NetworkFee = &networkFee // geth uses uint64 for gas diff --git a/pkg/service/user.go b/pkg/service/user.go index 49279e7d..62cda910 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -55,7 +55,7 @@ func (u user) GetStatus(request UserRequest) (model.UserOnboardingStatus, error) if err != nil { return res, common.StringError(err) } - associatedUser, err := u.repos.User.GetID(instrument.UserID) + associatedUser, err := u.repos.User.GetById(instrument.UserID) if err != nil { return res, common.StringError(err) } @@ -129,7 +129,7 @@ func (u user) Sign(request UserRequest) error { if instrument.UserID == "" { return common.StringError(errors.New("wallet not associated with user")) } - _, err = u.repos.User.GetID(instrument.UserID) // Don't need to update user, just verify they exist + _, err = u.repos.User.GetById(instrument.UserID) // Don't need to update user, just verify they exist if err != nil { return common.StringError(err) } @@ -236,7 +236,7 @@ func (u user) Name(request UserRequest) error { if instrument.UserID == "" { return common.StringError(errors.New("wallet not associated with user")) } - user, err := u.repos.User.GetID(instrument.UserID) + user, err := u.repos.User.GetById(instrument.UserID) if err != nil { return common.StringError(err) } diff --git a/pkg/test/stubs/repository.go b/pkg/test/stubs/repository.go index 144b7df1..73a430eb 100644 --- a/pkg/test/stubs/repository.go +++ b/pkg/test/stubs/repository.go @@ -39,7 +39,7 @@ import ( // return model.Asset{}, nil // } -// func (Asset) GetID(id string) (model.Asset, error) { +// func (Asset) GetById(id string) (model.Asset, error) { // if id == "1" { // return avax, nil // }