diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ab370a7 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +#Serve Http +PORT=7123 +HOST=127.0.0.1 + +#PostgreSQL +DB_HOST=localhost +DB_USER=postgres +DB_PASSWORD=strongpass +DB_NAME=urdt_ussd +DB_PORT=5432 +DB_SSLMODE=disable +DB_TIMEZONE=Africa/Nairobi + +#External API Calls +CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create +TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/ +BALANCE_URL=https://custodial.sarafu.africa/api/account/status/ +TRACK_URL=http://localhost:5003/api/v2/account/status diff --git a/cmd/africastalking/main.go b/cmd/africastalking/main.go index c24c4b1..db66a2e 100644 --- a/cmd/africastalking/main.go +++ b/cmd/africastalking/main.go @@ -16,9 +16,12 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" httpserver "git.grassecon.net/urdt/ussd/internal/http" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -26,6 +29,10 @@ var ( scriptDir = path.Join("services", "registration") ) +func init() { + initializers.LoadEnvVariables() +} + type atRequestParser struct{} func (arp *atRequestParser) GetSessionId(rq any) (string, error) { @@ -65,29 +72,34 @@ func (arp *atRequestParser) GetInput(rq any) ([]byte, error) { } func main() { + config.LoadConfig() + var dbDir string var resourceDir string var size uint + var database string var engineDebug bool var host string var port uint flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") + flag.StringVar(&database, "db", "gdbm", "database to be used") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", "127.0.0.1", "http host") - flag.UintVar(&port, "p", 7123, "http port") + flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") + flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") flag.Parse() logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() + ctx = context.WithValue(ctx, "Database", database) pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ Root: "root", OutputSize: uint32(size), - FlagCount: uint32(16), + FlagCount: uint32(128), } if engineDebug { @@ -119,7 +131,7 @@ func main() { os.Exit(1) } - lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) + lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) lhs.SetDataStore(&userdataStore) if err != nil { @@ -127,7 +139,8 @@ func main() { os.Exit(1) } - hl, err := lhs.GetHandler() + accountService := remote.AccountService{} + hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) diff --git a/cmd/async/main.go b/cmd/async/main.go index 09236fd..e4c94b0 100644 --- a/cmd/async/main.go +++ b/cmd/async/main.go @@ -13,8 +13,11 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -22,6 +25,10 @@ var ( scriptDir = path.Join("services", "registration") ) +func init() { + initializers.LoadEnvVariables() +} + type asyncRequestParser struct { sessionId string input []byte @@ -36,31 +43,36 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) { } func main() { + config.LoadConfig() + var sessionId string var dbDir string var resourceDir string var size uint + var database string var engineDebug bool var host string var port uint flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") + flag.StringVar(&database, "db", "gdbm", "database to be used") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", "127.0.0.1", "http host") - flag.UintVar(&port, "p", 7123, "http port") + flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") + flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") flag.Parse() logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) ctx := context.Background() + ctx = context.WithValue(ctx, "Database", database) pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ Root: "root", OutputSize: uint32(size), - FlagCount: uint32(16), + FlagCount: uint32(128), } if engineDebug { @@ -92,10 +104,11 @@ func main() { os.Exit(1) } - lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) + lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) lhs.SetDataStore(&userdataStore) + accountService := remote.AccountService{} - hl, err := lhs.GetHandler() + hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) diff --git a/cmd/http/main.go b/cmd/http/main.go index 6b868ed..96e2688 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -15,9 +15,12 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" httpserver "git.grassecon.net/urdt/ussd/internal/http" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -25,30 +28,39 @@ var ( scriptDir = path.Join("services", "registration") ) +func init() { + initializers.LoadEnvVariables() +} + func main() { + config.LoadConfig() + var dbDir string var resourceDir string var size uint + var database string var engineDebug bool var host string var port uint flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") + flag.StringVar(&database, "db", "gdbm", "database to be used") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", "127.0.0.1", "http host") - flag.UintVar(&port, "p", 7123, "http port") + flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") + flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") flag.Parse() logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() + ctx = context.WithValue(ctx, "Database", database) pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ Root: "root", OutputSize: uint32(size), - FlagCount: uint32(16), + FlagCount: uint32(128), } if engineDebug { @@ -80,7 +92,7 @@ func main() { os.Exit(1) } - lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) + lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) lhs.SetDataStore(&userdataStore) if err != nil { @@ -88,7 +100,8 @@ func main() { os.Exit(1) } - hl, err := lhs.GetHandler() + accountService := remote.AccountService{} + hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) diff --git a/cmd/main.go b/cmd/main.go index 9db5e0a..9599eb7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,8 +10,11 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -19,12 +22,20 @@ var ( scriptDir = path.Join("services", "registration") ) +func init() { + initializers.LoadEnvVariables() +} + func main() { + config.LoadConfig() + var dbDir string var size uint var sessionId string + var database string var engineDebug bool flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") + flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") @@ -34,13 +45,14 @@ func main() { ctx := context.Background() ctx = context.WithValue(ctx, "SessionId", sessionId) + ctx = context.WithValue(ctx, "Database", database) pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ Root: "root", SessionId: sessionId, OutputSize: uint32(size), - FlagCount: uint32(16), + FlagCount: uint32(128), } resourceDir := scriptDir @@ -76,7 +88,7 @@ func main() { os.Exit(1) } - lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) + lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) lhs.SetDataStore(&userdatastore) lhs.SetPersister(pe) @@ -85,7 +97,8 @@ func main() { os.Exit(1) } - hl, err := lhs.GetHandler() + accountService := remote.AccountService{} + hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) diff --git a/internal/utils/db.go b/common/db.go similarity index 65% rename from internal/utils/db.go rename to common/db.go index 410da68..1992476 100644 --- a/internal/utils/db.go +++ b/common/db.go @@ -1,7 +1,9 @@ -package utils +package common import ( "encoding/binary" + + "git.defalsify.org/vise.git/logging" ) type DataTyp uint16 @@ -22,7 +24,18 @@ const ( DATA_OFFERINGS DATA_RECIPIENT DATA_AMOUNT - DATA_TEMPORARY_PIN + DATA_TEMPORARY_VALUE + DATA_ACTIVE_SYM + DATA_ACTIVE_BAL + DATA_BLOCKED_NUMBER + DATA_PUBLIC_KEY_REVERSE + DATA_ACTIVE_DECIMAL + DATA_ACTIVE_ADDRESS + DATA_TRANSACTIONS +) + +var ( + logg = logging.NewVanilla().WithDomain("urdt-common") ) func typToBytes(typ DataTyp) []byte { diff --git a/common/hex.go b/common/hex.go new file mode 100644 index 0000000..971ecf1 --- /dev/null +++ b/common/hex.go @@ -0,0 +1,31 @@ +package common + +import ( + "encoding/hex" + "strings" +) + +func NormalizeHex(s string) (string, error) { + if len(s) >= 2 { + if s[:2] == "0x" { + s = s[2:] + } + } + r, err := hex.DecodeString(s) + if err != nil { + return "", err + } + return hex.EncodeToString(r), nil +} + +func IsSameHex(left string, right string) bool { + bl, err := NormalizeHex(left) + if err != nil { + return false + } + br, err := NormalizeHex(left) + if err != nil { + return false + } + return strings.Compare(bl, br) == 0 +} diff --git a/common/storage.go b/common/storage.go new file mode 100644 index 0000000..dff4774 --- /dev/null +++ b/common/storage.go @@ -0,0 +1,52 @@ +package common + +import ( + "context" + "errors" + + "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/persist" + "git.grassecon.net/urdt/ussd/internal/storage" +) + +func StoreToDb(store *UserDataStore) db.Db { + return store.Db +} + +func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb { + return storage.NewSubPrefixDb(store.Db, pfx) +} + +type StorageServices interface { + GetPersister(ctx context.Context) (*persist.Persister, error) + GetUserdataDb(ctx context.Context) (db.Db, error) + GetResource(ctx context.Context) (resource.Resource, error) + EnsureDbDir() error +} + +type StorageService struct { + svc *storage.MenuStorageService +} + +func NewStorageService(dbDir string) *StorageService { + return &StorageService{ + svc: storage.NewMenuStorageService(dbDir, ""), + } +} + +func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { + return ss.svc.GetPersister(ctx) +} + +func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { + return ss.svc.GetUserdataDb(ctx) +} + +func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { + return nil, errors.New("not implemented") +} + +func(ss *StorageService) EnsureDbDir() error { + return ss.svc.EnsureDbDir() +} diff --git a/internal/utils/userStore.go b/common/user_store.go similarity index 83% rename from internal/utils/userStore.go rename to common/user_store.go index a1485b1..29796e2 100644 --- a/internal/utils/userStore.go +++ b/common/user_store.go @@ -1,4 +1,4 @@ -package utils +package common import ( "context" @@ -16,7 +16,7 @@ type UserDataStore struct { db.Db } -// ReadEntry retrieves an entry from the store based on the provided parameters. +// ReadEntry retrieves an entry to the userdata store. func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { store.SetPrefix(db.DATATYPE_USERDATA) store.SetSession(sessionId) @@ -24,6 +24,8 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ return store.Get(ctx, k) } +// WriteEntry adds an entry to the userdata store. +// BUG: this uses sessionId twice func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { store.SetPrefix(db.DATATYPE_USERDATA) store.SetSession(sessionId) diff --git a/common/vouchers.go b/common/vouchers.go new file mode 100644 index 0000000..2fed043 --- /dev/null +++ b/common/vouchers.go @@ -0,0 +1,157 @@ +package common + +import ( + "context" + "fmt" + "strings" + + "git.grassecon.net/urdt/ussd/internal/storage" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// VoucherMetadata helps organize data fields +type VoucherMetadata struct { + Symbols string + Balances string + Decimals string + Addresses string +} + +// ProcessVouchers converts holdings into formatted strings +func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { + var data VoucherMetadata + var symbols, balances, decimals, addresses []string + + for i, h := range holdings { + symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) + balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance)) + decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals)) + addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress)) + } + + data.Symbols = strings.Join(symbols, "\n") + data.Balances = strings.Join(balances, "\n") + data.Decimals = strings.Join(decimals, "\n") + data.Addresses = strings.Join(addresses, "\n") + + return data +} + +//func StoreVouchers(db storage.PrefixDb, data VoucherMetadata) { +// value, err := db.Put(ctx, []byte(key)) +// if err != nil { +// return nil, fmt.Errorf("failed to get %s: %v", key, err) +// } +// data[key] = string(value) +// } +//} + +// GetVoucherData retrieves and matches voucher data +func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { + keys := []string{"sym", "bal", "deci", "addr"} + data := make(map[string]string) + + for _, key := range keys { + value, err := db.Get(ctx, []byte(key)) + if err != nil { + return nil, fmt.Errorf("failed to get %s: %v", key, err) + } + data[key] = string(value) + } + + symbol, balance, decimal, address := MatchVoucher(input, + data["sym"], + data["bal"], + data["deci"], + data["addr"]) + + if symbol == "" { + return nil, nil + } + + return &dataserviceapi.TokenHoldings{ + TokenSymbol: string(symbol), + Balance: string(balance), + TokenDecimals: string(decimal), + ContractAddress: string(address), + }, nil +} + +// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input. +func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) { + symList := strings.Split(symbols, "\n") + balList := strings.Split(balances, "\n") + decList := strings.Split(decimals, "\n") + addrList := strings.Split(addresses, "\n") + + logg.Tracef("found" , "symlist", symList, "syms", symbols, "input", input) + for i, sym := range symList { + parts := strings.SplitN(sym, ":", 2) + + if input == parts[0] || strings.EqualFold(input, parts[1]) { + symbol = parts[1] + if i < len(balList) { + balance = strings.SplitN(balList[i], ":", 2)[1] + } + if i < len(decList) { + decimal = strings.SplitN(decList[i], ":", 2)[1] + } + if i < len(addrList) { + address = strings.SplitN(addrList[i], ":", 2)[1] + } + break + } + } + return +} + +// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore. +func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { + tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress) + + if err := store.WriteEntry(ctx, sessionId, DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil { + return err + } + + return nil +} + +// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore. +func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) { + temp_data, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE) + if err != nil { + return nil, err + } + + values := strings.SplitN(string(temp_data), ",", 4) + + data := &dataserviceapi.TokenHoldings{} + + data.TokenSymbol = values[0] + data.Balance = values[1] + data.TokenDecimals = values[2] + data.ContractAddress = values[3] + + return data, nil +} + +// UpdateVoucherData sets the active voucher data and clears the temporary voucher data in the DataStore. +func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { + logg.TraceCtxf(ctx, "dtal", "data", data) + // Active voucher data entries + activeEntries := map[DataTyp][]byte{ + DATA_ACTIVE_SYM: []byte(data.TokenSymbol), + DATA_ACTIVE_BAL: []byte(data.Balance), + DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals), + DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress), + } + + // Write active data + for key, value := range activeEntries { + if err := store.WriteEntry(ctx, sessionId, key, value); err != nil { + return err + } + } + + return nil +} diff --git a/common/vouchers_test.go b/common/vouchers_test.go new file mode 100644 index 0000000..8b9fa2a --- /dev/null +++ b/common/vouchers_test.go @@ -0,0 +1,197 @@ +package common + +import ( + "context" + "fmt" + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/stretchr/testify/require" + + "git.grassecon.net/urdt/ussd/internal/storage" + memdb "git.defalsify.org/vise.git/db/mem" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// InitializeTestDb sets up and returns an in-memory database and store. +func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) { + ctx := context.Background() + + // Initialize memDb + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + require.NoError(t, err, "Failed to connect to memDb") + + // Create UserDataStore with memDb + store := &UserDataStore{Db: db} + + t.Cleanup(func() { + db.Close() // Ensure the DB is closed after each test + }) + + return ctx, store +} + +func TestMatchVoucher(t *testing.T) { + symbols := "1:SRF\n2:MILO" + balances := "1:100\n2:200" + decimals := "1:6\n2:4" + addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa" + + // Test for valid voucher + symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses) + + // Assertions for valid voucher + assert.Equal(t, "MILO", symbol) + assert.Equal(t, "200", balance) + assert.Equal(t, "4", decimal) + assert.Equal(t, "0x41c188d63Qa", address) + + // Test for non-existent voucher + symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses) + + // Assertions for non-match + assert.Equal(t, "", symbol) + assert.Equal(t, "", balance) + assert.Equal(t, "", decimal) + assert.Equal(t, "", address) +} + +func TestProcessVouchers(t *testing.T) { + holdings := []dataserviceapi.TokenHoldings{ + {ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"}, + {ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"}, + } + + expectedResult := VoucherMetadata{ + Symbols: "1:SRF\n2:MILO", + Balances: "1:100\n2:200", + Decimals: "1:6\n2:4", + Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa", + } + + result := ProcessVouchers(holdings) + + assert.Equal(t, expectedResult, result) +} + +func TestGetVoucherData(t *testing.T) { + ctx := context.Background() + + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) + + // Test voucher data + mockData := map[string][]byte{ + "sym": []byte("1:SRF\n2:MILO"), + "bal": []byte("1:100\n2:200"), + "deci": []byte("1:6\n2:4"), + "addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), + } + + // Put the data + for key, value := range mockData { + err = spdb.Put(ctx, []byte(key), []byte(value)) + if err != nil { + t.Fatal(err) + } + } + + result, err := GetVoucherData(ctx, spdb, "1") + + assert.NoError(t, err) + assert.Equal(t, "SRF", result.TokenSymbol) + assert.Equal(t, "100", result.Balance) + assert.Equal(t, "6", result.TokenDecimals) + assert.Equal(t, "0xd4c288865Ce", result.ContractAddress) +} + +func TestStoreTemporaryVoucher(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // Test data + voucherData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + // Execute the function being tested + err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData) + require.NoError(t, err) + + // Verify stored data + expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9") + + storedValue, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE) + require.NoError(t, err) + require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", DATA_TEMPORARY_VALUE) +} + +func TestGetTemporaryVoucherData(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // Test voucher data + tempData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + // Store the data + err := StoreTemporaryVoucher(ctx, store, sessionId, tempData) + require.NoError(t, err) + + // Execute the function being tested + data, err := GetTemporaryVoucherData(ctx, store, sessionId) + require.NoError(t, err) + require.Equal(t, tempData, data) +} + +func TestUpdateVoucherData(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // New voucher data + newData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + // Old temporary data + tempData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "OLD", + Balance: "100", + TokenDecimals: "8", + ContractAddress: "0xold", + } + require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData)) + + // Execute update + err := UpdateVoucherData(ctx, store, sessionId, newData) + require.NoError(t, err) + + // Verify active data was stored correctly + activeEntries := map[DataTyp][]byte{ + DATA_ACTIVE_SYM: []byte(newData.TokenSymbol), + DATA_ACTIVE_BAL: []byte(newData.Balance), + DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals), + DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress), + } + + for key, expectedValue := range activeEntries { + storedValue, err := store.ReadEntry(ctx, sessionId, key) + require.NoError(t, err) + require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key) + } +} diff --git a/config/config.go b/config/config.go index 0571503..fbf518b 100644 --- a/config/config.go +++ b/config/config.go @@ -1,10 +1,70 @@ package config +import ( + "net/url" - -const ( - CreateAccountURL = "https://custodial.sarafu.africa/api/account/create" - TrackStatusURL = "https://custodial.sarafu.africa/api/track/" - BalanceURL = "https://custodial.sarafu.africa/api/account/status/" + "git.grassecon.net/urdt/ussd/initializers" ) +const ( + createAccountPath = "/api/v2/account/create" + trackStatusPath = "/api/track" + balancePathPrefix = "/api/account" + trackPath = "/api/v2/account/status" + voucherHoldingsPathPrefix = "/api/v1/holdings" + voucherTransfersPathPrefix = "/api/v1/transfers/last10" + voucherDataPathPrefix = "/api/v1/token" +) + +var ( + custodialURLBase string + dataURLBase string + CustodialAPIKey string + DataAPIKey string +) + +var ( + CreateAccountURL string + TrackStatusURL string + BalanceURL string + TrackURL string + VoucherHoldingsURL string + VoucherTransfersURL string + VoucherDataURL string +) + +func setBase() error { + var err error + + custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003") + dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") + CustodialAPIKey = initializers.GetEnv("CUSTODIAL_API_KEY", "xd") + DataAPIKey = initializers.GetEnv("DATA_API_KEY", "xd") + + _, err = url.JoinPath(custodialURLBase, "/foo") + if err != nil { + return err + } + _, err = url.JoinPath(dataURLBase, "/bar") + if err != nil { + return err + } + return nil +} + +// LoadConfig initializes the configuration values after environment variables are loaded. +func LoadConfig() error { + err := setBase() + if err != nil { + return err + } + CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath) + TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath) + BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix) + TrackURL, _ = url.JoinPath(custodialURLBase, trackPath) + VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) + VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) + VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) + + return nil +} diff --git a/devtools/admin/admin_numbers.json b/devtools/admin/admin_numbers.json new file mode 100644 index 0000000..ca58a23 --- /dev/null +++ b/devtools/admin/admin_numbers.json @@ -0,0 +1,7 @@ +{ + "admins": [ + { + "phonenumber" : "" + } + ] +} \ No newline at end of file diff --git a/devtools/admin/commands/seed.go b/devtools/admin/commands/seed.go new file mode 100644 index 0000000..e76c83d --- /dev/null +++ b/devtools/admin/commands/seed.go @@ -0,0 +1,47 @@ +package commands + +import ( + "context" + "encoding/json" + "os" + + "git.defalsify.org/vise.git/logging" + "git.grassecon.net/urdt/ussd/internal/utils" +) + +var ( + logg = logging.NewVanilla().WithDomain("adminstore") +) + +type Admin struct { + PhoneNumber string `json:"phonenumber"` +} + +type Config struct { + Admins []Admin `json:"admins"` +} + +func Seed(ctx context.Context) error { + var config Config + adminstore, err := utils.NewAdminStore(ctx, "../admin_numbers") + store := adminstore.FsStore + if err != nil { + return err + } + defer store.Close() + data, err := os.ReadFile("admin_numbers.json") + if err != nil { + return err + } + if err := json.Unmarshal(data, &config); err != nil { + return err + } + for _, admin := range config.Admins { + err := store.Put(ctx, []byte(admin.PhoneNumber), []byte("1")) + if err != nil { + logg.Printf(logging.LVL_DEBUG, "Failed to insert admin number", admin.PhoneNumber) + return err + } + } + return nil +} diff --git a/devtools/admin/main.go b/devtools/admin/main.go new file mode 100644 index 0000000..9a527f3 --- /dev/null +++ b/devtools/admin/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "context" + "log" + + "git.grassecon.net/urdt/ussd/devtools/admin/commands" +) + +func main() { + ctx := context.Background() + err := commands.Seed(ctx) + if err != nil { + log.Fatalf("Failed to initialize a list of admins with error %s", err) + } + +} diff --git a/go.mod b/go.mod index c4c5167..391c1a5 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,43 @@ module git.grassecon.net/urdt/ussd -go 1.22.6 +go 1.23.0 + +toolchain go1.23.2 require ( - git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb + git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b github.com/alecthomas/assert/v2 v2.2.2 + github.com/grassrootseconomics/eth-custodial v1.3.0-beta github.com/peteole/testdata-loader v0.3.0 gopkg.in/leonelquinteros/gotext.v1 v1.3.1 ) +require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joho/godotenv v1.5.1 + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.18.0 // indirect +) + require ( github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/gofrs/uuid v4.4.0+incompatible github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.9.0 github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index ed5636f..0ba38c1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= +git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= +git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= @@ -8,29 +8,67 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= +github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= +github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU= +github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= +github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s= +github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/initializers/load.go b/initializers/load.go new file mode 100644 index 0000000..4ea5980 --- /dev/null +++ b/initializers/load.go @@ -0,0 +1,34 @@ +package initializers + +import ( + "log" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +func LoadEnvVariables() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } +} + +// Helper to get environment variables with a default fallback +func GetEnv(key, defaultVal string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultVal +} + +// Helper to safely convert environment variables to uint +func GetEnvUint(key string, defaultVal uint) uint { + if value, exists := os.LookupEnv(key); exists { + if parsed, err := strconv.Atoi(value); err == nil && parsed >= 0 { + return uint(parsed) + } + } + return defaultVal +} diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 4cedd26..7d8325c 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -1,12 +1,17 @@ package handlers import ( + "context" + "git.defalsify.org/vise.git/asm" "git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/internal/handlers/ussd" + "git.grassecon.net/urdt/ussd/internal/utils" + "git.grassecon.net/urdt/ussd/remote" ) type HandlerService interface { @@ -27,20 +32,26 @@ type LocalHandlerService struct { DbRs *resource.DbResource Pe *persist.Persister UserdataStore *db.Db + AdminStore *utils.AdminStore Cfg engine.Config Rs resource.Resource } -func NewLocalHandlerService(fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { +func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { parser, err := getParser(fp, debug) if err != nil { return nil, err } + adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") + if err != nil { + return nil, err + } return &LocalHandlerService{ - Parser: parser, - DbRs: dbResource, - Cfg: cfg, - Rs: rs, + Parser: parser, + DbRs: dbResource, + AdminStore: adminstore, + Cfg: cfg, + Rs: rs, }, nil } @@ -52,16 +63,16 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) { ls.UserdataStore = db } -func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { - ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore) +func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) { + ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService) if err != nil { return nil, err } ussdHandlers = ussdHandlers.WithPersister(ls.Pe) ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) - ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) - ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) + ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) + ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin) ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) @@ -82,17 +93,28 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) - ls.DbRs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) - ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) + ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances) + ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher) + ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) + ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) + ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher) + ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher) + ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin) + ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch) + ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber) + ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber) + ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber) + ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) + ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) return ussdHandlers, nil } diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go deleted file mode 100644 index f4375a1..0000000 --- a/internal/handlers/server/accountservice.go +++ /dev/null @@ -1,112 +0,0 @@ -package server - -import ( - "encoding/json" - "io" - "net/http" - - "git.grassecon.net/urdt/ussd/config" - "git.grassecon.net/urdt/ussd/internal/models" -) - -type AccountServiceInterface interface { - CheckBalance(publicKey string) (string, error) - CreateAccount() (*models.AccountResponse, error) - CheckAccountStatus(trackingId string) (string, error) -} - -type AccountService struct { -} - - - -// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID. -// -// Parameters: -// - trackingId: A unique identifier for the account.This should be obtained from a previous call to -// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the -// AccountResponse struct can be used here to check the account status during a transaction. -// -// -// Returns: -// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string. -// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. -// If no error occurs, this will be nil. -// -func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) { - resp, err := http.Get(config.TrackStatusURL + trackingId) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var trackResp models.TrackStatusResponse - err = json.Unmarshal(body, &trackResp) - if err != nil { - return "", err - } - - status := trackResp.Result.Transaction.Status - - return status, nil -} - - -// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. -// Parameters: -// - publicKey: The public key associated with the account whose balance needs to be checked. -func (as *AccountService) CheckBalance(publicKey string) (string, error) { - - resp, err := http.Get(config.BalanceURL + publicKey) - if err != nil { - return "0.0", err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "0.0", err - } - - var balanceResp models.BalanceResponse - err = json.Unmarshal(body, &balanceResp) - if err != nil { - return "0.0", err - } - - balance := balanceResp.Result.Balance - return balance, nil -} - - -//CreateAccount creates a new account in the custodial system. -// Returns: -// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account. -// If there is an error during the request or processing, this will be nil. -// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. -// If no error occurs, this will be nil. -func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { - resp, err := http.Post(config.CreateAccountURL, "application/json", nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var accountResp models.AccountResponse - err = json.Unmarshal(body, &accountResp) - if err != nil { - return nil, err - } - - return &accountResp, nil -} diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 8ffecc3..6c1917d 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -10,6 +10,7 @@ import ( "strings" "git.defalsify.org/vise.git/asm" + "github.com/grassrootseconomics/eth-custodial/pkg/api" "git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/db" @@ -18,15 +19,26 @@ import ( "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" - "git.grassecon.net/urdt/ussd/internal/handlers/server" + "git.grassecon.net/urdt/ussd/common" "git.grassecon.net/urdt/ussd/internal/utils" + "git.grassecon.net/urdt/ussd/remote" "gopkg.in/leonelquinteros/gotext.v1" + + "git.grassecon.net/urdt/ussd/internal/storage" ) var ( logg = logging.NewVanilla().WithDomain("ussdmenuhandler") scriptDir = path.Join("services", "registration") translationDir = path.Join(scriptDir, "locale") + okResponse *api.OKResponse + errResponse *api.ErrResponse +) + +// Define the regex patterns as constants +const ( + phoneRegex = `(\(\d{3}\)\s?|\d{3}[-.\s]?)?\d{3}[-.\s]?\d{4}` + pinPattern = `^\d{4}$` ) // FlagManager handles centralized flag management @@ -56,35 +68,44 @@ type Handlers struct { pe *persist.Persister st *state.State ca cache.Memory - userdataStore utils.DataStore + userdataStore common.DataStore + adminstore *utils.AdminStore flagManager *asm.FlagParser - accountService server.AccountServiceInterface + accountService remote.AccountServiceInterface + prefixDb storage.PrefixDb } -func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, error) { +func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) { if userdataStore == nil { return nil, fmt.Errorf("cannot create handler with nil userdata store") } - userDb := &utils.UserDataStore{ + userDb := &common.UserDataStore{ Db: userdataStore, } + // Instantiate the SubPrefixDb with "vouchers" prefix + prefixDb := storage.NewSubPrefixDb(userdataStore, []byte("vouchers")) + h := &Handlers{ userdataStore: userDb, flagManager: appFlags, - accountService: &server.AccountService{}, + adminstore: adminstore, + accountService: accountService, + prefixDb: prefixDb, } return h, nil } -// Define the regex pattern as a constant -const pinPattern = `^\d{4}$` - // isValidPIN checks whether the given input is a 4 digit number func isValidPIN(pin string) bool { match, _ := regexp.MatchString(pinPattern, pin) return match } +func isValidPhoneNumber(phonenumber string) bool { + match, _ := regexp.MatchString(phoneRegex, phonenumber) + return match +} + func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { if h.pe != nil { panic("persister already set") @@ -95,13 +116,25 @@ func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) { var r resource.Result - if h.pe == nil { logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca) return r, nil } + h.st = h.pe.GetState() h.ca = h.pe.GetMemory() + + sessionId, _ := ctx.Value("SessionId").(string) + flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege") + + isAdmin, _ := h.adminstore.IsAdmin(sessionId) + + if isAdmin { + r.FlagSet = append(r.FlagSet, flag_admin_privilege) + } else { + r.FlagReset = append(r.FlagReset, flag_admin_privilege) + } + if h.st == nil || h.ca == nil { logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca) return r, fmt.Errorf("cannot get state and memory for handler") @@ -136,23 +169,35 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r } func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error { - accountResp, err := h.accountService.CreateAccount() - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: accountResp.Result.TrackingId, - utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey, - utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(), + flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") + r, err := h.accountService.CreateAccount(ctx) + if err != nil { + return err } + trackingId := r.TrackingId + publicKey := r.PublicKey + data := map[common.DataTyp]string{ + common.DATA_TRACKING_ID: trackingId, + common.DATA_PUBLIC_KEY: publicKey, + } + store := h.userdataStore for key, value := range data { - store := h.userdataStore - err := store.WriteEntry(ctx, sessionId, key, []byte(value)) + err = store.WriteEntry(ctx, sessionId, key, []byte(value)) if err != nil { return err } } - flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") + publicKeyNormalized, err := common.NormalizeHex(publicKey) + if err != nil { + return err + } + err = store.WriteEntry(ctx, publicKeyNormalized, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)) + if err != nil { + return err + } res.FlagSet = append(res.FlagSet, flag_account_created) - return err + return nil } @@ -167,7 +212,7 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) return res, fmt.Errorf("missing session") } store := h.userdataStore - _, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_CREATED) + _, err = store.ReadEntry(ctx, sessionId, common.DATA_ACCOUNT_CREATED) if err != nil { if db.IsNotFound(err) { logg.Printf(logging.LVL_INFO, "Creating an account because it doesn't exist") @@ -180,31 +225,27 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) return res, nil } -// SavePin persists the user's PIN choice into the filesystem -func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) { - var res resource.Result - var err error - +func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch") sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") - - accountPIN := string(input) - // Validate that the PIN is a 4-digit number - if !isValidPIN(accountPIN) { - res.FlagSet = append(res.FlagSet, flag_incorrect_pin) - return res, nil - } - - res.FlagReset = append(res.FlagReset, flag_incorrect_pin) store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN)) + blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) if err != nil { return res, err } + temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE) + if err != nil { + return res, err + } + if bytes.Equal(temporaryPin, input) { + res.FlagReset = append(res.FlagReset, flag_pin_mismatch) + } else { + res.FlagSet = append(res.FlagSet, flag_pin_mismatch) + } return res, nil } @@ -226,6 +267,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) ( return res, nil } +// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_VALUE +// during the account creation process +// and during the change PIN process func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result var err error @@ -234,8 +278,8 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt if !ok { return res, fmt.Errorf("missing session") } - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") + flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") accountPIN := string(input) // Validate that the PIN is a 4-digit number @@ -243,11 +287,36 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt res.FlagSet = append(res.FlagSet, flag_incorrect_pin) return res, nil } + res.FlagReset = append(res.FlagReset, flag_incorrect_pin) store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN)) + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(accountPIN)) if err != nil { return res, err } + + return res, nil +} + +func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var err error + + store := h.userdataStore + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + temporaryPin := string(input) + blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) + + if err != nil { + return res, err + } + err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin)) + if err != nil { + return res, err + } + return res, nil } @@ -260,7 +329,7 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch") store := h.userdataStore - temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN) + temporaryPin, err := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) if err != nil { return res, err } @@ -269,17 +338,17 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt } else { res.FlagSet = append(res.FlagSet, flag_pin_mismatch) } - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin)) + err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin)) if err != nil { return res, err } return res, nil } -// VerifyPin checks whether the confirmation PIN is similar to the account PIN -// If similar, it sets the USERFLAG_PIN_SET flag allowing the user +// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN +// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user // to access the main menu -func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { +func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin") @@ -290,15 +359,12 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res if !ok { return res, fmt.Errorf("missing session") } - - //AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN) store := h.userdataStore - AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN) + temporaryPin, err := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) if err != nil { return res, err } - - if bytes.Equal(input, AccountPin) { + if bytes.Equal(input, temporaryPin) { res.FlagSet = []uint32{flag_valid_pin} res.FlagReset = []uint32{flag_pin_mismatch} res.FlagSet = append(res.FlagSet, flag_pin_set) @@ -306,6 +372,11 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res res.FlagSet = []uint32{flag_pin_mismatch} } + err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin)) + if err != nil { + return res, err + } + return res, nil } @@ -327,10 +398,18 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte) if !ok { return res, fmt.Errorf("missing session") } - if len(input) > 0 { - firstName := string(input) - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName)) + firstName := string(input) + store := h.userdataStore + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + if allowUpdate { + temporaryFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_FIRST_NAME, []byte(temporaryFirstName)) + if err != nil { + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(firstName)) if err != nil { return res, err } @@ -348,17 +427,24 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte) return res, fmt.Errorf("missing session") } - if len(input) > 0 { - familyName := string(input) - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(familyName)) + store := h.userdataStore + familyName := string(input) + + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + + if allowUpdate { + temporaryFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_FAMILY_NAME, []byte(temporaryFamilyName)) if err != nil { return res, err } } else { - return res, fmt.Errorf("a family name cannot be less than one character") + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(familyName)) + if err != nil { + return res, err + } } - return res, nil } @@ -370,11 +456,19 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou if !ok { return res, fmt.Errorf("missing session") } + yob := string(input) + store := h.userdataStore + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) - if len(input) == 4 { - yob := string(input) - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_YOB, []byte(yob)) + if allowUpdate { + temporaryYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_YOB, []byte(temporaryYob)) + if err != nil { + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(yob)) if err != nil { return res, err } @@ -391,11 +485,20 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) ( if !ok { return res, fmt.Errorf("missing session") } + location := string(input) + store := h.userdataStore - if len(input) > 0 { - location := string(input) - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_LOCATION, []byte(location)) + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + + if allowUpdate { + temporaryLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_LOCATION, []byte(temporaryLocation)) + if err != nil { + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(location)) if err != nil { return res, err } @@ -413,12 +516,22 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re if !ok { return res, fmt.Errorf("missing session") } - gender := strings.Split(symbol, "_")[1] store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) - if err != nil { - return res, nil + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + + if allowUpdate { + temporaryGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_GENDER, []byte(temporaryGender)) + if err != nil { + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(gender)) + if err != nil { + return res, err + } } return res, nil @@ -433,12 +546,22 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) return res, fmt.Errorf("missing session") } - if len(input) > 0 { - offerings := string(input) - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_OFFERINGS, []byte(offerings)) + offerings := string(input) + store := h.userdataStore + + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + + if allowUpdate { + temporaryOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_OFFERINGS, []byte(temporaryOfferings)) if err != nil { - return res, nil + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(offerings)) + if err != nil { + return res, err } } @@ -455,10 +578,17 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt return res, nil } +// ResetAllowUpdate resets the allowupdate flag that allows a user to update profile data. +func (h *Handlers) ResetValidPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin") + res.FlagReset = append(res.FlagReset, flag_valid_pin) + return res, nil +} + // ResetAccountAuthorized resets the account authorization flag after a successful PIN entry. func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") res.FlagReset = append(res.FlagReset, flag_account_authorized) @@ -468,14 +598,12 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input // CheckIdentifier retrieves the PublicKey from the JSON data file. func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - store := h.userdataStore - publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + publicKey, _ := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) res.Content = string(publicKey) @@ -487,18 +615,16 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result var err error - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") store := h.userdataStore - AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN) + AccountPin, err := store.ReadEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN) if err != nil { return res, err } @@ -525,9 +651,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res // ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt. func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") - res.FlagReset = append(res.FlagReset, flag_incorrect_pin) return res, nil } @@ -539,29 +663,29 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b flag_account_success, _ := h.flagManager.GetFlag("flag_account_success") flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } + store := h.userdataStore - trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID) + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) if err != nil { return res, err } + r, err := h.accountService.TrackAccountStatus(ctx, string(publicKey)) - status, err := h.accountService.CheckAccountStatus(string(trackingId)) if err != nil { - fmt.Println("Error checking account status:", err) + res.FlagSet = append(res.FlagSet, flag_api_error) return res, err } - - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)) - if err != nil { - return res, nil + res.FlagReset = append(res.FlagReset, flag_api_error) + if !ok { + return res, err } - - if status == "SUCCESS" { + if r.Active { res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagReset = append(res.FlagReset, flag_account_pending) } else { @@ -607,7 +731,6 @@ func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (res var err error flag_incorrect_date_format, _ := h.flagManager.GetFlag("flag_incorrect_date_format") - date := string(input) _, err = strconv.Atoi(date) if err != nil { @@ -630,12 +753,11 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by var res resource.Result flag_incorrect_date_format, _ := h.flagManager.GetFlag("flag_incorrect_date_format") - res.FlagReset = append(res.FlagReset, flag_incorrect_date_format) return res, nil } -// CheckBalance retrieves the balance from the API using the "PublicKey" and sets +// CheckBalance retrieves the balance of the active voucher and sets // the balance as the result content func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -646,18 +768,130 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) ( return res, fmt.Errorf("missing session") } + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + store := h.userdataStore - publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + + // get the active sym and active balance + activeSym, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) + if err != nil { + if db.IsNotFound(err) { + balance := "0.00" + res.Content = l.Get("Balance: %s\n", balance) + return res, nil + } + + return res, err + } + + activeBal, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_BAL) if err != nil { return res, err } - balance, err := h.accountService.CheckBalance(string(publicKey)) + res.Content = l.Get("Balance: %s\n", fmt.Sprintf("%s %s", activeBal, activeSym)) + + return res, nil +} + +func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + symbol, _ := h.st.Where() + balanceType := strings.Split(symbol, "_")[0] + + store := h.userdataStore + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) + if err != nil { + return res, err + } + + balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + return res, nil + } + res.FlagReset = append(res.FlagReset, flag_api_error) + + balance := balanceResponse.Balance + + switch balanceType { + case "my": + res.Content = fmt.Sprintf("Your balance is %s", balance) + case "community": + res.Content = fmt.Sprintf("Your community balance is %s", balance) + default: + break + } + return res, nil +} + +func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + store := h.userdataStore + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + blockedPhonenumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) + if err != nil { + return res, err + } + temporaryPin, err := store.ReadEntry(ctx, string(blockedPhonenumber), common.DATA_TEMPORARY_VALUE) + if err != nil { + return res, err + } + err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(temporaryPin)) if err != nil { return res, nil } - res.Content = balance + return res, nil +} +func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number") + res.FlagReset = append(res.FlagReset, flag_unregistered_number) + return res, nil +} + +func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var err error + + flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number") + store := h.userdataStore + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + blockedNumber := string(input) + _, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY) + if !isValidPhoneNumber(blockedNumber) { + res.FlagSet = append(res.FlagSet, flag_unregistered_number) + return res, nil + } + if err != nil { + if db.IsNotFound(err) { + logg.Printf(logging.LVL_INFO, "Invalid or unregistered number") + res.FlagSet = append(res.FlagSet, flag_unregistered_number) + return res, nil + } else { + return res, err + } + } + err = store.WriteEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER, []byte(blockedNumber)) + if err != nil { + return res, nil + } return res, nil } @@ -684,7 +918,7 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by return res, nil } store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_RECIPIENT, []byte(recipient)) + err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recipient)) if err != nil { return res, nil } @@ -707,12 +941,12 @@ func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byt flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte("")) + err = store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte("")) if err != nil { return res, nil } - err = store.WriteEntry(ctx, sessionId, utils.DATA_RECIPIENT, []byte("")) + err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte("")) if err != nil { return res, nil } @@ -734,7 +968,7 @@ func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte("")) + err = store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte("")) if err != nil { return res, nil } @@ -755,14 +989,13 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res return res, fmt.Errorf("missing session") } store := h.userdataStore - publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) - balance, err := h.accountService.CheckBalance(string(publicKey)) + activeBal, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_BAL) if err != nil { - return res, nil + return res, err } - res.Content = balance + res.Content = string(activeBal) return res, nil } @@ -771,47 +1004,29 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res // it is not more than the current balance. func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - var err error sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") - store := h.userdataStore - publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) - amountStr := string(input) - - balanceStr, err := h.accountService.CheckBalance(string(publicKey)) + var balanceValue float64 + // retrieve the active balance + activeBal, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_BAL) if err != nil { return res, err } - res.Content = balanceStr - - // Parse the balance - balanceParts := strings.Split(balanceStr, " ") - if len(balanceParts) != 2 { - return res, fmt.Errorf("unexpected balance format: %s", balanceStr) - } - balanceValue, err := strconv.ParseFloat(balanceParts[0], 64) + balanceValue, err = strconv.ParseFloat(string(activeBal), 64) if err != nil { - return res, fmt.Errorf("failed to parse balance: %v", err) + return res, err } - // Extract numeric part from input - re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`) - matches := re.FindStringSubmatch(strings.TrimSpace(amountStr)) - if len(matches) < 2 { - res.FlagSet = append(res.FlagSet, flag_invalid_amount) - res.Content = amountStr - return res, nil - } - - inputAmount, err := strconv.ParseFloat(matches[1], 64) + // Extract numeric part from the input amount + amountStr := strings.TrimSpace(string(input)) + inputAmount, err := strconv.ParseFloat(amountStr, 64) if err != nil { res.FlagSet = append(res.FlagSet, flag_invalid_amount) res.Content = amountStr @@ -824,12 +1039,14 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) return res, nil } - res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places - err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr)) + // Format the amount with 2 decimal places before saving + formattedAmount := fmt.Sprintf("%.2f", inputAmount) + err = store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(formattedAmount)) if err != nil { return res, err } + res.Content = fmt.Sprintf("%s", formattedAmount) return res, nil } @@ -842,14 +1059,30 @@ func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) ( return res, fmt.Errorf("missing session") } store := h.userdataStore - recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT) + recipient, _ := store.ReadEntry(ctx, sessionId, common.DATA_RECIPIENT) res.Content = string(recipient) return res, nil } -// GetSender retrieves the public key from the Gdbm Db +// RetrieveBlockedNumber gets the current number during the pin reset for other's is in progress. +func (h *Handlers) RetrieveBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + store := h.userdataStore + blockedNumber, _ := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) + + res.Content = string(blockedNumber) + + return res, nil +} + +// GetSender returns the sessionId (phoneNumber) func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -858,10 +1091,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res return res, fmt.Errorf("missing session") } - store := h.userdataStore - publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) - - res.Content = string(publicKey) + res.Content = string(sessionId) return res, nil } @@ -875,40 +1105,17 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res return res, fmt.Errorf("missing session") } store := h.userdataStore - amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT) - res.Content = string(amount) - - return res, nil -} - -// QuickWithBalance retrieves the balance for a given public key from the custodial balance API endpoint before -// gracefully exiting the session. -func (h *Handlers) QuitWithBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { - var res resource.Result - var err error - sessionId, ok := ctx.Value("SessionId").(string) - if !ok { - return res, fmt.Errorf("missing session") - } - - flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") - - code := codeFromCtx(ctx) - l := gotext.NewLocale(translationDir, code) - l.AddDomain("default") - - store := h.userdataStore - publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + // retrieve the active symbol + activeSym, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) if err != nil { return res, err } - balance, err := h.accountService.CheckBalance(string(publicKey)) - if err != nil { - return res, nil - } - res.Content = l.Get("Your account balance is %s", balance) - res.FlagReset = append(res.FlagReset, flag_account_authorized) + + amount, _ := store.ReadEntry(ctx, sessionId, common.DATA_AMOUNT) + + res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym)) + return res, nil } @@ -928,13 +1135,14 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input [] // TODO // Use the amount, recipient and sender to call the API and initialize the transaction store := h.userdataStore - publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) - amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT) + amount, _ := store.ReadEntry(ctx, sessionId, common.DATA_AMOUNT) - recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT) + recipient, _ := store.ReadEntry(ctx, sessionId, common.DATA_RECIPIENT) - res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(publicKey)) + activeSym, _ := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) + + res.Content = l.Get("Your request has been sent. %s will receive %s %s from %s.", string(recipient), string(amount), string(activeSym), string(sessionId)) account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized") if err != nil { @@ -945,16 +1153,23 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input [] return res, nil } -// GetProfileInfo retrieves and formats the profile information of a user from a Gdbm backed storage. func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result + var defaultValue string sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - - // Default value when an entry is not found - defaultValue := "Not Provided" + language, ok := ctx.Value("Language").(lang.Language) + if !ok { + return res, fmt.Errorf("value for 'Language' is not of type lang.Language") + } + code := language.Code + if code == "swa" { + defaultValue = "Haipo" + } else { + defaultValue = "Not Provided" + } // Helper function to handle nil byte slices and convert them to string getEntryOrDefault := func(entry []byte, err error) string { @@ -965,12 +1180,12 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) } store := h.userdataStore // Retrieve user data as strings with fallback to defaultValue - firstName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_FIRST_NAME)) - familyName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_FAMILY_NAME)) - yob := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_YOB)) - gender := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_GENDER)) - location := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_LOCATION)) - offerings := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_OFFERINGS)) + firstName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)) + familyName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)) + yob := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_YOB)) + gender := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_GENDER)) + location := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)) + offerings := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)) // Construct the full name name := defaultValue @@ -991,12 +1206,201 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) return res, fmt.Errorf("invalid year of birth: %v", err) } } - - // Format the result - res.Content = fmt.Sprintf( - "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", - name, gender, age, location, offerings, - ) + switch language.Code { + case "eng": + res.Content = fmt.Sprintf( + "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", + name, gender, age, location, offerings, + ) + case "swa": + res.Content = fmt.Sprintf( + "Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", + name, gender, age, location, offerings, + ) + default: + res.Content = fmt.Sprintf( + "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", + name, gender, age, location, offerings, + ) + } return res, nil } + +// SetDefaultVoucher retrieves the current vouchers +// and sets the first as the default voucher, if no active voucher is set +func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var err error + store := h.userdataStore + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher") + + // check if the user has an active sym + _, err = store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) + + if err != nil { + if db.IsNotFound(err) { + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) + if err != nil { + return res, err + } + + // Fetch vouchers from the API using the public key + vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey)) + if err != nil { + return res, err + } + + // Return if there is no voucher + if len(vouchersResp) == 0 { + res.FlagSet = append(res.FlagSet, flag_no_active_voucher) + return res, nil + } + + // Use only the first voucher + firstVoucher := vouchersResp[0] + defaultSym := firstVoucher.TokenSymbol + defaultBal := firstVoucher.Balance + + // set the active symbol + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(defaultSym)) + if err != nil { + return res, err + } + // set the active balance + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_BAL, []byte(defaultBal)) + if err != nil { + return res, err + } + + return res, nil + } + + return res, err + } + + res.FlagReset = append(res.FlagReset, flag_no_active_voucher) + + return res, nil +} + +// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores +// them to gdbm +func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + store := h.userdataStore + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) + if err != nil { + return res, nil + } + + // Fetch vouchers from the API using the public key + vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey)) + if err != nil { + return res, nil + } + + data := common.ProcessVouchers(vouchersResp) + + // Store all voucher data + dataMap := map[string]string{ + "sym": data.Symbols, + "bal": data.Balances, + "deci": data.Decimals, + "addr": data.Addresses, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != nil { + return res, nil + } + } + + return res, nil +} + +// GetVoucherList fetches the list of vouchers and formats them +func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + + // Read vouchers from the store + voucherData, err := h.prefixDb.Get(ctx, []byte("sym")) + if err != nil { + return res, err + } + + res.Content = string(voucherData) + + return res, nil +} + +// ViewVoucher retrieves the token holding and balance from the subprefixDB +func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") + + inputStr := string(input) + if inputStr == "0" || inputStr == "99" { + res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) + return res, nil + } + + metadata, err := common.GetVoucherData(ctx, h.prefixDb, inputStr) + if err != nil { + return res, fmt.Errorf("failed to retrieve voucher data: %v", err) + } + + if metadata == nil { + res.FlagSet = append(res.FlagSet, flag_incorrect_voucher) + return res, nil + } + + if err := common.StoreTemporaryVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil { + return res, err + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) + res.Content = fmt.Sprintf("%s\n%s", metadata.TokenSymbol, metadata.Balance) + + return res, nil +} + +// SetVoucher retrieves the temp voucher data and sets it as the active data +func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + // Get temporary data + tempData, err := common.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId) + if err != nil { + return res, err + } + + // Set as active and clear temporary data + if err := common.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil { + return res, err + } + + res.Content = tempData.TokenSymbol + return res, nil +} diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 83e6f0c..f4c9f7c 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -8,16 +8,23 @@ import ( "path" "testing" - "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" - "git.grassecon.net/urdt/ussd/internal/mocks" - "git.grassecon.net/urdt/ussd/internal/models" - "git.grassecon.net/urdt/ussd/internal/utils" + "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/internal/testutil/mocks" + "git.grassecon.net/urdt/ussd/internal/testutil/testservice" + "git.grassecon.net/urdt/ussd/models" + + "git.grassecon.net/urdt/ussd/common" "github.com/alecthomas/assert/v2" + testdataloader "github.com/peteole/testdata-loader" "github.com/stretchr/testify/require" + + memdb "git.defalsify.org/vise.git/db/mem" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" ) var ( @@ -25,75 +32,125 @@ var ( flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") ) -func TestCreateAccount(t *testing.T) { +// InitializeTestStore sets up and returns an in-memory database and store. +func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) { + ctx := context.Background() + + // Initialize memDb + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + require.NoError(t, err, "Failed to connect to memDb") + + // Create UserDataStore with memDb + store := &common.UserDataStore{Db: db} + + t.Cleanup(func() { + db.Close() // Ensure the DB is closed after each test + }) + + return ctx, store +} + +func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb { + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) + + return spdb +} + +func TestNewHandlers(t *testing.T) { + _, store := InitializeTestStore(t) fm, err := NewFlagManager(flagsPath) - + accountService := testservice.TestAccountService{} if err != nil { t.Logf(err.Error()) } + t.Run("Valid UserDataStore", func(t *testing.T) { + handlers, err := NewHandlers(fm.parser, store, nil, &accountService) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if handlers == nil { + t.Fatal("expected handlers to be non-nil") + } + if handlers.userdataStore == nil { + t.Fatal("expected userdataStore to be set in handlers") + } + }) - // Create required mocks - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - expectedResult := resource.Result{} - accountCreatedFlag, err := fm.GetFlag("flag_account_created") + // Test case for nil userdataStore + t.Run("Nil UserDataStore", func(t *testing.T) { + handlers, err := NewHandlers(fm.parser, nil, nil, &accountService) + if err == nil { + t.Fatal("expected an error, got none") + } + if handlers != nil { + t.Fatal("expected handlers to be nil") + } + if err.Error() != "cannot create handler with nil userdata store" { + t.Fatalf("expected specific error, got %v", err) + } + }) +} - if err != nil { - t.Logf(err.Error()) - } - expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag) - - // Define session ID and mock data +func TestCreateAccount(t *testing.T) { sessionId := "session123" - typ := utils.DATA_ACCOUNT_CREATED - fakeError := db.ErrNotFound{} - // Create context with session ID - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError) - expectedAccountResp := &models.AccountResponse{ - Ok: true, - Result: struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - }{ - CustodialId: "12", - PublicKey: "0x8E0XSCSVA", - TrackingId: "d95a7e83-196c-4fd0-866fSGAGA", + fm, err := NewFlagManager(flagsPath) + if err != nil { + t.Logf(err.Error()) + } + + flag_account_created, err := fm.GetFlag("flag_account_created") + if err != nil { + t.Logf(err.Error()) + } + + tests := []struct { + name string + serverResponse *models.AccountResult + expectedResult resource.Result + }{ + { + name: "Test account creation success", + serverResponse: &models.AccountResult{ + TrackingId: "1234567890", + PublicKey: "0xD3adB33f", + }, + expectedResult: resource.Result{ + FlagSet: []uint32{flag_account_created}, + }, }, } - mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil) - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId, - utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey, - utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(), + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) + + h := &Handlers{ + userdataStore: store, + accountService: mockAccountService, + flagManager: fm.parser, + } + + mockAccountService.On("CreateAccount").Return(tt.serverResponse, nil) + + // Call the method you want to test + res, err := h.CreateAccount(ctx, "create_account", []byte("")) + + // Assert that no errors occurred + assert.NoError(t, err) + + // Assert that the account created flag has been set to the result + assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") + }) } - - for key, value := range data { - mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil) - } - - // Create a Handlers instance with the mock data store - h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, - flagManager: fm.parser, - } - - // Call the method you want to test - res, err := h.CreateAccount(ctx, "create_account", []byte("some-input")) - - // Assert that no errors occurred - assert.NoError(t, err) - - //Assert that the account created flag has been set to the result - assert.Equal(t, res, expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) } func TestWithPersister(t *testing.T) { @@ -116,20 +173,30 @@ func TestWithPersister_PanicWhenAlreadySet(t *testing.T) { } func TestSaveFirstname(t *testing.T) { - // Create a new instance of MockMyDataStore - mockStore := new(mocks.MockUserDataStore) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + + // Set the flag in the State + mockState := state.NewState(16) + mockState.SetFlag(flag_allow_update) // Define test data - sessionId := "session123" firstName := "John" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName)).Return(nil) + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(firstName)); err != nil { + t.Fatal(err) + } // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockStore, + userdataStore: store, + flagManager: fm.parser, + st: mockState, } // Call the method @@ -139,25 +206,36 @@ func TestSaveFirstname(t *testing.T) { assert.NoError(t, err) assert.Equal(t, resource.Result{}, res) - // Assert all expectations were met - mockStore.AssertExpectations(t) + // Verify that the DATA_FIRST_NAME entry has been updated with the temporary value + storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) + assert.Equal(t, firstName, string(storedFirstName)) } func TestSaveFamilyname(t *testing.T) { - // Create a new instance of UserDataStore - mockStore := new(mocks.MockUserDataStore) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + + // Set the flag in the State + mockState := state.NewState(16) + mockState.SetFlag(flag_allow_update) // Define test data - sessionId := "session123" familyName := "Doeee" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(familyName)).Return(nil) + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(familyName)); err != nil { + t.Fatal(err) + } // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockStore, + userdataStore: store, + st: mockState, + flagManager: fm.parser, } // Call the method @@ -167,25 +245,213 @@ func TestSaveFamilyname(t *testing.T) { assert.NoError(t, err) assert.Equal(t, resource.Result{}, res) - // Assert all expectations were met - mockStore.AssertExpectations(t) + // Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value + storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) + assert.Equal(t, familyName, string(storedFamilyName)) } -func TestSavePin(t *testing.T) { +func TestSaveYoB(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + + // Set the flag in the State + mockState := state.NewState(16) + mockState.SetFlag(flag_allow_update) + + // Define test data + yob := "1980" + + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(yob)); err != nil { + t.Fatal(err) + } + + // Create the Handlers instance with the mock store + h := &Handlers{ + userdataStore: store, + flagManager: fm.parser, + st: mockState, + } + + // Call the method + res, err := h.SaveYob(ctx, "save_yob", []byte(yob)) + + // Assert results + assert.NoError(t, err) + assert.Equal(t, resource.Result{}, res) + + // Verify that the DATA_YOB entry has been updated with the temporary value + storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB) + assert.Equal(t, yob, string(storedYob)) +} + +func TestSaveLocation(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + + // Set the flag in the State + mockState := state.NewState(16) + mockState.SetFlag(flag_allow_update) + + // Define test data + location := "Kilifi" + + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(location)); err != nil { + t.Fatal(err) + } + + // Create the Handlers instance with the mock store + h := &Handlers{ + userdataStore: store, + flagManager: fm.parser, + st: mockState, + } + + // Call the method + res, err := h.SaveLocation(ctx, "save_location", []byte(location)) + + // Assert results + assert.NoError(t, err) + assert.Equal(t, resource.Result{}, res) + + // Verify that the DATA_LOCATION entry has been updated with the temporary value + storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) + assert.Equal(t, location, string(storedLocation)) +} + +func TestSaveOfferings(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + + // Set the flag in the State + mockState := state.NewState(16) + mockState.SetFlag(flag_allow_update) + + // Define test data + offerings := "Bananas" + + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(offerings)); err != nil { + t.Fatal(err) + } + + // Create the Handlers instance with the mock store + h := &Handlers{ + userdataStore: store, + flagManager: fm.parser, + st: mockState, + } + + // Call the method + res, err := h.SaveOfferings(ctx, "save_offerings", []byte(offerings)) + + // Assert results + assert.NoError(t, err) + assert.Equal(t, resource.Result{}, res) + + // Verify that the DATA_OFFERINGS entry has been updated with the temporary value + storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) + assert.Equal(t, offerings, string(storedOfferings)) +} + +func TestSaveGender(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + + // Set the flag in the State + mockState := state.NewState(16) + mockState.SetFlag(flag_allow_update) + + // Define test cases + tests := []struct { + name string + input []byte + expectedGender string + executingSymbol string + }{ + { + name: "Valid Male Input", + input: []byte("1"), + expectedGender: "male", + executingSymbol: "set_male", + }, + { + name: "Valid Female Input", + input: []byte("2"), + expectedGender: "female", + executingSymbol: "set_female", + }, + { + name: "Valid Unspecified Input", + input: []byte("3"), + executingSymbol: "set_unspecified", + expectedGender: "unspecified", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(tt.expectedGender)); err != nil { + t.Fatal(err) + } + + mockState.ExecPath = append(mockState.ExecPath, tt.executingSymbol) + // Create the Handlers instance with the mock store + h := &Handlers{ + userdataStore: store, + st: mockState, + flagManager: fm.parser, + } + + // Call the method + res, err := h.SaveGender(ctx, "save_gender", tt.input) + + // Assert results + assert.NoError(t, err) + assert.Equal(t, resource.Result{}, res) + + // Verify that the DATA_GENDER entry has been updated with the temporary value + storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER) + assert.Equal(t, tt.expectedGender, string(storedGender)) + }) + } +} + +func TestSaveTemporaryPin(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + fm, err := NewFlagManager(flagsPath) - mockStore := new(mocks.MockUserDataStore) if err != nil { log.Fatal(err) } + flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") // Create the Handlers instance with the mock flag manager h := &Handlers{ flagManager: fm.parser, - userdataStore: mockStore, + userdataStore: store, } - sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) // Define test cases tests := []struct { @@ -211,199 +477,34 @@ func TestSavePin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.input)).Return(nil) - // Call the method - res, err := h.SavePin(ctx, "save_pin", tt.input) + res, err := h.SaveTemporaryPin(ctx, "save_pin", tt.input) if err != nil { t.Error(err) } - // Assert that the Result FlagSet has the required flags after language switch - assert.Equal(t, res, tt.expectedResult, "Flags should be equal to account created") - - }) - } -} - -func TestSaveYoB(t *testing.T) { - // Create a new instance of MockMyDataStore - mockStore := new(mocks.MockUserDataStore) - - // Define test data - sessionId := "session123" - yob := "1980" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_YOB, []byte(yob)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - } - - // Call the method - res, err := h.SaveYob(ctx, "save_yob", []byte(yob)) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) - - // Assert all expectations were met - mockStore.AssertExpectations(t) -} - -func TestSaveLocation(t *testing.T) { - // Create a new instance of MockMyDataStore - mockStore := new(mocks.MockUserDataStore) - - // Define test data - sessionId := "session123" - yob := "Kilifi" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_LOCATION, []byte(yob)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - } - - // Call the method - res, err := h.SaveLocation(ctx, "save_location", []byte(yob)) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) - - // Assert all expectations were met - mockStore.AssertExpectations(t) -} - -func TestSaveOfferings(t *testing.T) { - // Create a new instance of MockUserDataStore - mockStore := new(mocks.MockUserDataStore) - - // Define test data - sessionId := "session123" - offerings := "Bananas" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_OFFERINGS, []byte(offerings)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - } - - // Call the method - res, err := h.SaveOfferings(ctx, "save_offerings", []byte(offerings)) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) - - // Assert all expectations were met - mockStore.AssertExpectations(t) -} - -func TestSaveGender(t *testing.T) { - // Create a new instance of MockMyDataStore - mockStore := new(mocks.MockUserDataStore) - mockState := state.NewState(16) - - // Define the session ID and context - sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Define test cases - tests := []struct { - name string - input []byte - expectedGender string - expectCall bool - executingSymbol string - }{ - { - name: "Valid Male Input", - input: []byte("1"), - expectedGender: "male", - executingSymbol: "set_male", - expectCall: true, - }, - { - name: "Valid Female Input", - input: []byte("2"), - expectedGender: "female", - executingSymbol: "set_female", - expectCall: true, - }, - { - name: "Valid Unspecified Input", - input: []byte("3"), - executingSymbol: "set_unspecified", - expectedGender: "unspecified", - expectCall: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Set up expectations for the mock database - if tt.expectCall { - expectedKey := utils.DATA_GENDER - mockStore.On("WriteEntry", ctx, sessionId, expectedKey, []byte(tt.expectedGender)).Return(nil) - } else { - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_GENDER, []byte(tt.expectedGender)).Return(nil) - } - mockState.ExecPath = append(mockState.ExecPath, tt.executingSymbol) - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - st: mockState, - } - - // Call the method - _, err := h.SaveGender(ctx, "save_gender", tt.input) - - // Assert no error - assert.NoError(t, err) - - // Verify expectations - if tt.expectCall { - mockStore.AssertCalled(t, "WriteEntry", ctx, sessionId, utils.DATA_GENDER, []byte(tt.expectedGender)) - } else { - mockStore.AssertNotCalled(t, "WriteEntry", ctx, sessionId, utils.DATA_GENDER, []byte(tt.expectedGender)) - } + assert.Equal(t, res, tt.expectedResult, "Result should match expected result") }) } } func TestCheckIdentifier(t *testing.T) { - // Create a new instance of MockMyDataStore - mockStore := new(mocks.MockUserDataStore) - - // Define the session ID and context sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) // Define test cases tests := []struct { name string - mockPublicKey []byte + publicKey []byte mockErr error expectedContent string expectError bool }{ { name: "Saved public Key", - mockPublicKey: []byte("0xa8363"), + publicKey: []byte("0xa8363"), mockErr: nil, expectedContent: "0xa8363", expectError: false, @@ -412,137 +513,105 @@ func TestCheckIdentifier(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Set up expectations for the mock database - mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.mockPublicKey, tt.mockErr) + err := store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(tt.publicKey)) + if err != nil { + t.Fatal(err) + } // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockStore, + userdataStore: store, } // Call the method res, err := h.CheckIdentifier(ctx, "check_identifier", nil) // Assert results - assert.NoError(t, err) assert.Equal(t, tt.expectedContent, res.Content) - - // Verify expectations - mockStore.AssertExpectations(t) }) } } -func TestMaxAmount(t *testing.T) { - mockStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - - // Define test data - sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - publicKey := "0xcasgatweksalw1018221" - expectedBalance := "0.003CELO" - - // Set up the expected behavior of the mock - mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) - mockCreateAccountService.On("CheckBalance", publicKey).Return(expectedBalance, nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - accountService: mockCreateAccountService, - } - - // Call the method - res, _ := h.MaxAmount(ctx, "max_amount", []byte("check_balance")) - - //Assert that the balance that was set as the result content is what was returned by Check Balance - assert.Equal(t, expectedBalance, res.Content) - -} - func TestGetSender(t *testing.T) { - mockStore := new(mocks.MockUserDataStore) - - // Define test data sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - publicKey := "0xcasgatweksalw1018221" + ctx, _ := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) - // Set up the expected behavior of the mock - mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - } + // Create the Handlers instance + h := &Handlers{} // Call the method - res, _ := h.GetSender(ctx, "max_amount", []byte("check_balance")) - - //Assert that the public key from readentry operation is what was set as the result content. - assert.Equal(t, publicKey, res.Content) + res, _ := h.GetSender(ctx, "get_sender", []byte("")) + //Assert that the sessionId is what was set as the result content. + assert.Equal(t, sessionId, res.Content) } func TestGetAmount(t *testing.T) { - mockStore := new(mocks.MockUserDataStore) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) // Define test data - sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - Amount := "0.03CELO" + amount := "0.03" + activeSym := "SRF" - // Set up the expected behavior of the mock - mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(Amount), nil) + err := store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(amount)) + if err != nil { + t.Fatal(err) + } + + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(activeSym)) + if err != nil { + t.Fatal(err) + } // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockStore, + userdataStore: store, } // Call the method - res, _ := h.GetAmount(ctx, "get_amount", []byte("Getting amount...")) + res, _ := h.GetAmount(ctx, "get_amount", []byte("")) + + formattedAmount := fmt.Sprintf("%s %s", amount, activeSym) //Assert that the retrieved amount is what was set as the content - assert.Equal(t, Amount, res.Content) - + assert.Equal(t, formattedAmount, res.Content) } func TestGetRecipient(t *testing.T) { - mockStore := new(mocks.MockUserDataStore) - - // Define test data sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + recepient := "0xcasgatweksalw1018221" - // Set up the expected behavior of the mock - mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return([]byte(recepient), nil) + err := store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recepient)) + if err != nil { + t.Fatal(err) + } // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockStore, + userdataStore: store, } // Call the method - res, _ := h.GetRecipient(ctx, "get_recipient", []byte("Getting recipient...")) + res, _ := h.GetRecipient(ctx, "get_recipient", []byte("")) //Assert that the retrieved recepient is what was set as the content assert.Equal(t, recepient, res.Content) - } func TestGetFlag(t *testing.T) { fm, err := NewFlagManager(flagsPath) expectedFlag := uint32(9) - if err != nil { t.Logf(err.Error()) } flag, err := fm.GetFlag("flag_account_created") - if err != nil { t.Logf(err.Error()) } @@ -551,12 +620,11 @@ func TestGetFlag(t *testing.T) { } func TestSetLanguage(t *testing.T) { - // Create a new instance of the Flag Manager fm, err := NewFlagManager(flagsPath) - if err != nil { log.Fatal(err) } + // Define test cases tests := []struct { name string @@ -595,25 +663,24 @@ func TestSetLanguage(t *testing.T) { // Call the method res, err := h.SetLanguage(context.Background(), "set_language", nil) - if err != nil { t.Error(err) } // Assert that the Result FlagSet has the required flags after language switch assert.Equal(t, res, tt.expectedResult, "Result should match expected result") - }) } } + func TestResetAllowUpdate(t *testing.T) { fm, err := NewFlagManager(flagsPath) - - flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update") - if err != nil { log.Fatal(err) } + + flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update") + // Define test cases tests := []struct { name string @@ -631,7 +698,6 @@ func TestResetAllowUpdate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create the Handlers instance with the mock flag manager h := &Handlers{ flagManager: fm.parser, @@ -639,25 +705,24 @@ func TestResetAllowUpdate(t *testing.T) { // Call the method res, err := h.ResetAllowUpdate(context.Background(), "reset_allow update", tt.input) - if err != nil { t.Error(err) } + // Assert that the Result FlagSet has the required flags after language switch assert.Equal(t, res, tt.expectedResult, "Flags should be equal to account created") - }) } } func TestResetAccountAuthorized(t *testing.T) { fm, err := NewFlagManager(flagsPath) - - flag_account_authorized, _ := fm.parser.GetFlag("flag_account_authorized") - if err != nil { log.Fatal(err) } + + flag_account_authorized, _ := fm.parser.GetFlag("flag_account_authorized") + // Define test cases tests := []struct { name string @@ -675,7 +740,6 @@ func TestResetAccountAuthorized(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create the Handlers instance with the mock flag manager h := &Handlers{ flagManager: fm.parser, @@ -683,25 +747,24 @@ func TestResetAccountAuthorized(t *testing.T) { // Call the method res, err := h.ResetAccountAuthorized(context.Background(), "reset_account_authorized", tt.input) - if err != nil { t.Error(err) } + // Assert that the Result FlagSet has the required flags after language switch assert.Equal(t, res, tt.expectedResult, "Result should contain flag(s) that have been reset") - }) } } func TestIncorrectPinReset(t *testing.T) { fm, err := NewFlagManager(flagsPath) - - flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") - if err != nil { log.Fatal(err) } + + flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") + // Define test cases tests := []struct { name string @@ -719,7 +782,6 @@ func TestIncorrectPinReset(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create the Handlers instance with the mock flag manager h := &Handlers{ flagManager: fm.parser, @@ -727,25 +789,24 @@ func TestIncorrectPinReset(t *testing.T) { // Call the method res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input) - if err != nil { t.Error(err) } + // Assert that the Result FlagSet has the required flags after language switch assert.Equal(t, res, tt.expectedResult, "Result should contain flag(s) that have been reset") - }) } } func TestResetIncorrectYob(t *testing.T) { fm, err := NewFlagManager(flagsPath) - - flag_incorrect_date_format, _ := fm.parser.GetFlag("flag_incorrect_date_format") - if err != nil { log.Fatal(err) } + + flag_incorrect_date_format, _ := fm.parser.GetFlag("flag_incorrect_date_format") + // Define test cases tests := []struct { name string @@ -763,7 +824,6 @@ func TestResetIncorrectYob(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create the Handlers instance with the mock flag manager h := &Handlers{ flagManager: fm.parser, @@ -771,43 +831,39 @@ func TestResetIncorrectYob(t *testing.T) { // Call the method res, err := h.ResetIncorrectYob(context.Background(), "reset_incorrect_yob", tt.input) - if err != nil { t.Error(err) } + // Assert that the Result FlagSet has the required flags after language switch assert.Equal(t, res, tt.expectedResult, "Result should contain flag(s) that have been reset") - }) } } func TestAuthorize(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) fm, err := NewFlagManager(flagsPath) - if err != nil { t.Logf(err.Error()) } // Create required mocks - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - //expectedResult := resource.Result{} + mockAccountService := new(mocks.MockAccountService) mockState := state.NewState(16) flag_incorrect_pin, _ := fm.GetFlag("flag_incorrect_pin") flag_account_authorized, _ := fm.GetFlag("flag_account_authorized") flag_allow_update, _ := fm.GetFlag("flag_allow_update") - //Assuming 1234 is the correct account pin + + // Set 1234 is the correct account pin accountPIN := "1234" - // Define session ID and mock data - sessionId := "session123" - typ := utils.DATA_ACCOUNT_PIN - h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, st: mockState, } @@ -842,14 +898,10 @@ func TestAuthorize(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - // Create context with session ID - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(accountPIN), nil) - - // Create a Handlers instance with the mock data store + err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN)) + if err != nil { + t.Fatal(err) + } // Call the method under test res, err := h.Authorize(ctx, "authorize", []byte(tt.input)) @@ -859,33 +911,25 @@ func TestAuthorize(t *testing.T) { //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } - } func TestVerifyYob(t *testing.T) { fm, err := NewFlagManager(flagsPath) - if err != nil { t.Logf(err.Error()) } sessionId := "session123" // Create required mocks - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) mockState := state.NewState(16) flag_incorrect_date_format, _ := fm.parser.GetFlag("flag_incorrect_date_format") ctx := context.WithValue(context.Background(), "SessionId", sessionId) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + accountService: mockAccountService, flagManager: fm.parser, st: mockState, } @@ -920,7 +964,6 @@ func TestVerifyYob(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Call the method under test res, err := h.VerifyYob(ctx, "verify_yob", []byte(tt.input)) @@ -929,37 +972,31 @@ func TestVerifyYob(t *testing.T) { //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } } -func TestVerifyPin(t *testing.T) { - fm, err := NewFlagManager(flagsPath) +func TestVerifyCreatePin(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + fm, err := NewFlagManager(flagsPath) if err != nil { t.Logf(err.Error()) } - sessionId := "session123" // Create required mocks - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) mockState := state.NewState(16) flag_valid_pin, _ := fm.parser.GetFlag("flag_valid_pin") flag_pin_mismatch, _ := fm.parser.GetFlag("flag_pin_mismatch") flag_pin_set, _ := fm.parser.GetFlag("flag_pin_set") - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - //Assuming this was the first set PIN to verify against - firstSetPin := "1234" h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, st: mockState, } @@ -986,113 +1023,114 @@ func TestVerifyPin(t *testing.T) { }, } - typ := utils.DATA_ACCOUNT_PIN - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil) + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte("1234")) + if err != nil { + t.Fatal(err) + } // Call the method under test - res, err := h.VerifyPin(ctx, "verify_pin", []byte(tt.input)) + res, err := h.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input)) // Assert that no errors occurred assert.NoError(t, err) //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } } func TestCheckAccountStatus(t *testing.T) { - fm, err := NewFlagManager(flagsPath) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + fm, err := NewFlagManager(flagsPath) if err != nil { t.Logf(err.Error()) } - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - - sessionId := "session123" flag_account_success, _ := fm.GetFlag("flag_account_success") flag_account_pending, _ := fm.GetFlag("flag_account_pending") + flag_api_error, _ := fm.GetFlag("flag_api_call_error") - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, - flagManager: fm.parser, - } tests := []struct { name string - input []byte - status string + publicKey []byte + response *models.TrackStatusResult expectedResult resource.Result }{ { - name: "Test when account status is Success", - input: []byte("TrackingId1234"), - status: "SUCCESS", + name: "Test when account is on the Sarafu network", + publicKey: []byte("TrackingId1234"), + response: &models.TrackStatusResult{ + Active: true, + }, expectedResult: resource.Result{ FlagSet: []uint32{flag_account_success}, - FlagReset: []uint32{flag_account_pending}, + FlagReset: []uint32{flag_api_error, flag_account_pending}, + }, + }, + { + name: "Test when the account is not yet on the sarafu network", + publicKey: []byte("TrackingId1234"), + response: &models.TrackStatusResult{ + Active: false, + }, + expectedResult: resource.Result{ + FlagSet: []uint32{flag_account_pending}, + FlagReset: []uint32{flag_api_error, flag_account_success}, }, }, } - - typ := utils.DATA_TRACKING_ID for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return(tt.input, nil) + h := &Handlers{ + userdataStore: store, + accountService: mockAccountService, + flagManager: fm.parser, + } - mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.status, nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(tt.status)).Return(nil) + err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(tt.publicKey)) + if err != nil { + t.Fatal(err) + } + + mockAccountService.On("TrackAccountStatus", string(tt.publicKey)).Return(tt.response, nil) // Call the method under test - res, _ := h.CheckAccountStatus(ctx, "check_status", tt.input) + res, _ := h.CheckAccountStatus(ctx, "check_account_status", []byte("")) // Assert that no errors occurred assert.NoError(t, err) //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } - } func TestTransactionReset(t *testing.T) { - fm, err := NewFlagManager(flagsPath) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + fm, err := NewFlagManager(flagsPath) if err != nil { t.Logf(err.Error()) } + flag_invalid_recipient, _ := fm.GetFlag("flag_invalid_recipient") flag_invalid_recipient_with_invite, _ := fm.GetFlag("flag_invalid_recipient_with_invite") - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - - sessionId := "session123" - - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, } tests := []struct { @@ -1111,9 +1149,6 @@ func TestTransactionReset(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, []byte("")).Return(nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_RECIPIENT, []byte("")).Return(nil) - // Call the method under test res, _ := h.TransactionReset(ctx, "transaction_reset", tt.input) @@ -1122,40 +1157,32 @@ func TestTransactionReset(t *testing.T) { //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } } -func TestResetInvalidAmount(t *testing.T) { +func TestResetTransactionAmount(t *testing.T) { sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) fm, err := NewFlagManager(flagsPath) - if err != nil { t.Logf(err.Error()) } flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, } tests := []struct { name string - input []byte - status string expectedResult resource.Result }{ { @@ -1167,73 +1194,71 @@ func TestResetInvalidAmount(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, []byte("")).Return(nil) - // Call the method under test - res, _ := h.ResetTransactionAmount(ctx, "transaction_reset_amount", tt.input) + res, _ := h.ResetTransactionAmount(ctx, "transaction_reset_amount", []byte("")) // Assert that no errors occurred assert.NoError(t, err) //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } - } func TestInitiateTransaction(t *testing.T) { - sessionId := "session123" + sessionId := "254712345678" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) fm, err := NewFlagManager(flagsPath) - if err != nil { t.Logf(err.Error()) } - account_authorized_flag, err := fm.parser.GetFlag("flag_account_authorized") + account_authorized_flag, _ := fm.parser.GetFlag("flag_account_authorized") - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, } tests := []struct { name string input []byte - PublicKey []byte Recipient []byte Amount []byte + ActiveSym []byte status string expectedResult resource.Result }{ { - name: "Test amount reset", - PublicKey: []byte("0x1241527192"), - Amount: []byte("0.002CELO"), + name: "Test initiate transaction", + Amount: []byte("0.002"), + ActiveSym: []byte("SRF"), Recipient: []byte("0x12415ass27192"), expectedResult: resource.Result{ FlagReset: []uint32{account_authorized_flag}, - Content: "Your request has been sent. 0x12415ass27192 will receive 0.002CELO from 0x1241527192.", + Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.PublicKey, nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil) - //mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, []byte("")).Return(nil) + err := store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.Amount)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(tt.Recipient)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(tt.ActiveSym)) + if err != nil { + t.Fatal(err) + } // Call the method under test res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input) @@ -1243,33 +1268,25 @@ func TestInitiateTransaction(t *testing.T) { //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } - } func TestQuit(t *testing.T) { fm, err := NewFlagManager(flagsPath) - if err != nil { t.Logf(err.Error()) } flag_account_authorized, _ := fm.parser.GetFlag("flag_account_authorized") - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) sessionId := "session123" ctx := context.WithValue(context.Background(), "SessionId", sessionId) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + accountService: mockAccountService, flagManager: fm.parser, } tests := []struct { @@ -1298,13 +1315,10 @@ func TestQuit(t *testing.T) { //Assert that the account created flag has been set to the result assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - }) } } + func TestIsValidPIN(t *testing.T) { tests := []struct { name string @@ -1358,116 +1372,54 @@ func TestIsValidPIN(t *testing.T) { } } -func TestQuitWithBalance(t *testing.T) { - fm, err := NewFlagManager(flagsPath) - - if err != nil { - t.Logf(err.Error()) - } - flag_account_authorized, _ := fm.parser.GetFlag("flag_account_authorized") - - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - - sessionId := "session123" - - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, - flagManager: fm.parser, - } - tests := []struct { - name string - input []byte - publicKey []byte - balance string - expectedResult resource.Result - }{ - { - name: "Test quit with balance", - balance: "0.02CELO", - publicKey: []byte("0xrqeqrequuq"), - expectedResult: resource.Result{ - FlagReset: []uint32{flag_account_authorized}, - Content: fmt.Sprintf("Your account balance is %s", "0.02CELO"), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) - mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil) - - // Call the method under test - res, _ := h.QuitWithBalance(ctx, "test_quit_with_balance", tt.input) - - // Assert that no errors occurred - assert.NoError(t, err) - - //Assert that the account created flag has been set to the result - assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - - }) - } -} - func TestValidateAmount(t *testing.T) { fm, err := NewFlagManager(flagsPath) - if err != nil { t.Logf(err.Error()) } - flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") + + mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, } tests := []struct { name string input []byte - publicKey []byte + activeBal []byte balance string expectedResult resource.Result }{ { name: "Test with valid amount", - input: []byte("0.001"), - balance: "0.003 CELO", - publicKey: []byte("0xrqeqrequuq"), + input: []byte("4.10"), + activeBal: []byte("5"), expectedResult: resource.Result{ - Content: "0.001", + Content: "4.10", }, }, { - name: "Test with amount larger than balance", - input: []byte("0.02"), - balance: "0.003 CELO", - publicKey: []byte("0xrqeqrequuq"), + name: "Test with amount larger than active balance", + input: []byte("5.02"), + activeBal: []byte("5"), expectedResult: resource.Result{ FlagSet: []uint32{flag_invalid_amount}, - Content: "0.02", + Content: "5.02", }, }, { - name: "Test with invalid amount", + name: "Test with invalid amount format", input: []byte("0.02ms"), - balance: "0.003 CELO", - publicKey: []byte("0xrqeqrequuq"), + activeBal: []byte("5"), expectedResult: resource.Result{ FlagSet: []uint32{flag_invalid_amount}, Content: "0.02ms", @@ -1477,40 +1429,35 @@ func TestValidateAmount(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) - mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe() + err := store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_BAL, []byte(tt.activeBal)) + if err != nil { + t.Fatal(err) + } // Call the method under test res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input) - // Assert that no errors occurred + // Assert no errors occurred assert.NoError(t, err) - //Assert that the account created flag has been set to the result - assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - + // Assert the result matches the expected result + assert.Equal(t, tt.expectedResult, res, "Expected result should match actual result") }) } } func TestValidateRecipient(t *testing.T) { fm, err := NewFlagManager(flagsPath) - - flag_invalid_recipient, _ := fm.parser.GetFlag("flag_invalid_recipient") - mockDataStore := new(mocks.MockUserDataStore) - - sessionId := "session123" - - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - if err != nil { log.Fatal(err) } + + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + flag_invalid_recipient, _ := fm.parser.GetFlag("flag_invalid_recipient") + // Define test cases tests := []struct { name string @@ -1534,13 +1481,10 @@ func TestValidateRecipient(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_RECIPIENT, tt.input).Return(nil) - - // Create the Handlers instance with the mock flag manager + // Create the Handlers instance h := &Handlers{ flagManager: fm.parser, - userdataStore: mockDataStore, + userdataStore: store, } // Call the method @@ -1549,66 +1493,118 @@ func TestValidateRecipient(t *testing.T) { if err != nil { t.Error(err) } + // Assert that the Result FlagSet has the required flags after language switch assert.Equal(t, res, tt.expectedResult, "Result should contain flag(s) that have been reset") - }) } } func TestCheckBalance(t *testing.T) { + ctx, store := InitializeTestStore(t) - mockDataStore := new(mocks.MockUserDataStore) - sessionId := "session123" - publicKey := "0X13242618721" - balance := "0.003 CELO" - - expectedResult := resource.Result{ - Content: "0.003 CELO", + tests := []struct { + name string + sessionId string + publicKey string + activeSym string + activeBal string + expectedResult resource.Result + expectError bool + }{ + { + name: "User with active sym", + sessionId: "session123", + publicKey: "0X98765432109", + activeSym: "ETH", + activeBal: "1.5", + expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"}, + expectError: false, + }, } - mockCreateAccountService := new(mocks.MockAccountService) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) + ctx := context.WithValue(ctx, "SessionId", tt.sessionId) - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + h := &Handlers{ + userdataStore: store, + accountService: mockAccountService, + } - h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, - //flagManager: fm.parser, + err := store.WriteEntry(ctx, tt.sessionId, common.DATA_ACTIVE_SYM, []byte(tt.activeSym)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, tt.sessionId, common.DATA_ACTIVE_BAL, []byte(tt.activeBal)) + if err != nil { + t.Fatal(err) + } + + res, err := h.CheckBalance(ctx, "check_balance", []byte("")) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, res, "Result should match expected output") + } + + mockAccountService.AssertExpectations(t) + }) } - //mock call operations - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) - mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(balance, nil) - - res, _ := h.CheckBalance(ctx, "check_balance", []byte("123456")) - - assert.Equal(t, res, expectedResult, "Result should contain flag(s) that have been reset") - } func TestGetProfile(t *testing.T) { - sessionId := "session123" + ctx, store := InitializeTestStore(t) - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) + mockState := state.NewState(16) h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, + st: mockState, } - ctx := context.WithValue(context.Background(), "SessionId", sessionId) tests := []struct { - name string - keys []utils.DataTyp - profileInfo []string - result resource.Result + name string + languageCode string + keys []common.DataTyp + profileInfo []string + result resource.Result }{ { - name: "Test with full profile information", - keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, - profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, + name: "Test with full profile information in eng", + keys: []common.DataTyp{common.DATA_FAMILY_NAME, common.DATA_FIRST_NAME, common.DATA_GENDER, common.DATA_OFFERINGS, common.DATA_LOCATION, common.DATA_YOB}, + profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, + languageCode: "eng", + result: resource.Result{ + Content: fmt.Sprintf( + "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", + "John Doee", "Male", "48", "Kilifi", "Bananas", + ), + }, + }, + { + name: "Test with with profile information in swa", + keys: []common.DataTyp{common.DATA_FAMILY_NAME, common.DATA_FIRST_NAME, common.DATA_GENDER, common.DATA_OFFERINGS, common.DATA_LOCATION, common.DATA_YOB}, + profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, + languageCode: "swa", + result: resource.Result{ + Content: fmt.Sprintf( + "Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", + "John Doee", "Male", "48", "Kilifi", "Bananas", + ), + }, + }, + { + name: "Test with with profile information with language that is not yet supported", + keys: []common.DataTyp{common.DATA_FAMILY_NAME, common.DATA_FIRST_NAME, common.DATA_GENDER, common.DATA_OFFERINGS, common.DATA_LOCATION, common.DATA_YOB}, + profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, + languageCode: "nor", result: resource.Result{ Content: fmt.Sprintf( "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", @@ -1619,17 +1615,21 @@ func TestGetProfile(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx = context.WithValue(ctx, "SessionId", sessionId) + ctx = context.WithValue(ctx, "Language", lang.Language{ + Code: tt.languageCode, + }) for index, key := range tt.keys { - mockDataStore.On("ReadEntry", ctx, sessionId, key).Return([]byte(tt.profileInfo[index]), nil) + err := store.WriteEntry(ctx, sessionId, key, []byte(tt.profileInfo[index])) + if err != nil { + t.Fatal(err) + } } - res, _ := h.GetProfileInfo(ctx, "get_profile_info", []byte("")) - // Assert that expectations were met - mockDataStore.AssertExpectations(t) + res, _ := h.GetProfileInfo(ctx, "get_profile_info", []byte("")) //Assert that the result set to content is what was expected assert.Equal(t, res, tt.result, "Result should contain profile information served back to user") - }) } } @@ -1640,12 +1640,10 @@ func TestVerifyNewPin(t *testing.T) { fm, _ := NewFlagManager(flagsPath) flag_valid_pin, _ := fm.parser.GetFlag("flag_valid_pin") - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, flagManager: fm.parser, - accountService: mockCreateAccountService, + accountService: mockAccountService, } ctx := context.WithValue(context.Background(), "SessionId", sessionId) @@ -1668,80 +1666,32 @@ func TestVerifyNewPin(t *testing.T) { FlagReset: []uint32{flag_valid_pin}, }, }, - { - name: "Test with invalid pin", - input: []byte("12345"), - expectedResult: resource.Result{ - FlagReset: []uint32{flag_valid_pin}, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - //Call the function under test res, _ := h.VerifyNewPin(ctx, "verify_new_pin", tt.input) - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - //Assert that the result set to content is what was expected assert.Equal(t, res, tt.expectedResult, "Result should contain flags set according to user input") - }) } - -} - -func TestSaveTemporaryPIn(t *testing.T) { - - fm, err := NewFlagManager(flagsPath) - - if err != nil { - t.Logf(err.Error()) - } - - // Create a new instance of UserDataStore - mockStore := new(mocks.MockUserDataStore) - - // Define test data - sessionId := "session123" - PIN := "1234" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - flagManager: fm.parser, - } - - // Call the method - res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN)) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) - - // Assert all expectations were met - mockStore.AssertExpectations(t) } func TestConfirmPin(t *testing.T) { sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + fm, _ := NewFlagManager(flagsPath) flag_pin_mismatch, _ := fm.parser.GetFlag("flag_pin_mismatch") - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, + userdataStore: store, flagManager: fm.parser, - accountService: mockCreateAccountService, + accountService: mockAccountService, } - ctx := context.WithValue(context.Background(), "SessionId", sessionId) tests := []struct { name string @@ -1761,20 +1711,285 @@ func TestConfirmPin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set up the expected behavior of the mock - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.temporarypin)).Return(nil) - - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN).Return(tt.temporarypin, nil) + err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(tt.temporarypin)) + if err != nil { + t.Fatal(err) + } //Call the function under test res, _ := h.ConfirmPinChange(ctx, "confirm_pin_change", tt.temporarypin) - // Assert that expectations were met - mockDataStore.AssertExpectations(t) - //Assert that the result set to content is what was expected assert.Equal(t, res, tt.expectedResult, "Result should contain flags set according to user input") }) } - +} + +func TestFetchCustodialBalances(t *testing.T) { + fm, err := NewFlagManager(flagsPath) + if err != nil { + t.Logf(err.Error()) + } + flag_api_error, _ := fm.GetFlag("flag_api_call_error") + + // Define test data + sessionId := "session123" + publicKey := "0X13242618721" + + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + balanceResponse *models.BalanceResult + expectedResult resource.Result + }{ + { + name: "Test when fetch custodial balances is not a success", + balanceResponse: &models.BalanceResult{ + Balance: "0.003 CELO", + Nonce: json.Number("0"), + }, + expectedResult: resource.Result{ + FlagReset: []uint32{flag_api_error}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) + mockState := state.NewState(16) + + h := &Handlers{ + userdataStore: store, + flagManager: fm.parser, + st: mockState, + accountService: mockAccountService, + } + + // Set up the expected behavior of the mock + mockAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil) + + // Call the method + res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte("")) + + //Assert that the result set to content is what was expected + assert.Equal(t, res, tt.expectedResult, "Result should match expected result") + }) + } +} + +func TestSetDefaultVoucher(t *testing.T) { + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + fm, err := NewFlagManager(flagsPath) + if err != nil { + t.Logf(err.Error()) + } + flag_no_active_voucher, err := fm.GetFlag("flag_no_active_voucher") + if err != nil { + t.Logf(err.Error()) + } + + publicKey := "0X13242618721" + + tests := []struct { + name string + vouchersResp []dataserviceapi.TokenHoldings + expectedResult resource.Result + }{ + { + name: "Test no vouchers available", + vouchersResp: []dataserviceapi.TokenHoldings{}, + expectedResult: resource.Result{ + FlagSet: []uint32{flag_no_active_voucher}, + }, + }, + { + name: "Test set default voucher when no active voucher is set", + vouchersResp: []dataserviceapi.TokenHoldings{ + dataserviceapi.TokenHoldings{ + ContractAddress: "0x123", + TokenSymbol: "TOKEN1", + TokenDecimals: "18", + Balance: "100", + }, + }, + expectedResult: resource.Result{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) + + h := &Handlers{ + userdataStore: store, + accountService: mockAccountService, + flagManager: fm.parser, + } + + err := store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + mockAccountService.On("FetchVouchers", string(publicKey)).Return(tt.vouchersResp, nil) + + res, err := h.SetDefaultVoucher(ctx, "set_default_voucher", []byte("some-input")) + + assert.NoError(t, err) + + assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") + + mockAccountService.AssertExpectations(t) + }) + } +} + +func TestCheckVouchers(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) + sessionId := "session123" + publicKey := "0X13242618721" + + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + spdb := InitializeTestSubPrefixDb(t, ctx) + + h := &Handlers{ + userdataStore: store, + accountService: mockAccountService, + prefixDb: spdb, + } + + err := store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + mockVouchersResponse := []dataserviceapi.TokenHoldings{ + {ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"}, + {ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"}, + } + + expectedSym := []byte("1:SRF\n2:MILO") + + mockAccountService.On("FetchVouchers", string(publicKey)).Return(mockVouchersResponse, nil) + + _, err = h.CheckVouchers(ctx, "check_vouchers", []byte("")) + assert.NoError(t, err) + + // Read voucher sym data from the store + voucherData, err := spdb.Get(ctx, []byte("sym")) + if err != nil { + t.Fatal(err) + } + + // assert that the data is stored correctly + assert.Equal(t, expectedSym, voucherData) + + mockAccountService.AssertExpectations(t) +} + +func TestGetVoucherList(t *testing.T) { + sessionId := "session123" + ctx := context.WithValue(context.Background(), "SessionId", sessionId) + + spdb := InitializeTestSubPrefixDb(t, ctx) + + h := &Handlers{ + prefixDb: spdb, + } + + expectedSym := []byte("1:SRF\n2:MILO") + + // Put voucher sym data from the store + err := spdb.Put(ctx, []byte("sym"), expectedSym) + if err != nil { + t.Fatal(err) + } + + res, err := h.GetVoucherList(ctx, "", []byte("")) + + assert.NoError(t, err) + assert.Equal(t, res.Content, string(expectedSym)) +} + +func TestViewVoucher(t *testing.T) { + fm, err := NewFlagManager(flagsPath) + if err != nil { + t.Logf(err.Error()) + } + ctx, store := InitializeTestStore(t) + sessionId := "session123" + + ctx = context.WithValue(ctx, "SessionId", sessionId) + + spdb := InitializeTestSubPrefixDb(t, ctx) + + h := &Handlers{ + userdataStore: store, + flagManager: fm.parser, + prefixDb: spdb, + } + + // Define mock voucher data + mockData := map[string][]byte{ + "sym": []byte("1:SRF\n2:MILO"), + "bal": []byte("1:100\n2:200"), + "deci": []byte("1:6\n2:4"), + "addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), + } + + // Put the data + for key, value := range mockData { + err = spdb.Put(ctx, []byte(key), []byte(value)) + if err != nil { + t.Fatal(err) + } + } + + res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1")) + assert.NoError(t, err) + assert.Equal(t, res.Content, "SRF\n100") +} + +func TestSetVoucher(t *testing.T) { + ctx, store := InitializeTestStore(t) + sessionId := "session123" + + ctx = context.WithValue(ctx, "SessionId", sessionId) + + h := &Handlers{ + userdataStore: store, + } + + // Define the temporary voucher data + tempData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + expectedData := fmt.Sprintf("%s,%s,%s,%s", tempData.TokenSymbol, tempData.Balance, tempData.TokenDecimals, tempData.ContractAddress) + + // store the expectedData + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(expectedData)); err != nil { + t.Fatal(err) + } + + res, err := h.SetVoucher(ctx, "set_voucher", []byte{}) + + assert.NoError(t, err) + + assert.Equal(t, string(tempData.TokenSymbol), res.Content) } diff --git a/internal/http/http_test.go b/internal/http/http_test.go index 8f6f312..14bb90a 100644 --- a/internal/http/http_test.go +++ b/internal/http/http_test.go @@ -13,7 +13,7 @@ import ( "git.defalsify.org/vise.git/engine" "git.grassecon.net/urdt/ussd/internal/handlers" - "git.grassecon.net/urdt/ussd/internal/mocks/httpmocks" + "git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" ) // invalidRequestType is a custom type to test invalid request scenarios diff --git a/internal/mocks/dbmock.go b/internal/mocks/dbmock.go deleted file mode 100644 index 0b40eab..0000000 --- a/internal/mocks/dbmock.go +++ /dev/null @@ -1,59 +0,0 @@ -package mocks - -import ( - "context" - - "git.defalsify.org/vise.git/lang" - "github.com/stretchr/testify/mock" -) - -type MockDb struct { - mock.Mock -} - -func (m *MockDb) SetPrefix(prefix uint8) { - m.Called(prefix) -} - -func (m *MockDb) Prefix() uint8 { - args := m.Called() - return args.Get(0).(uint8) -} - -func (m *MockDb) Safe() bool { - args := m.Called() - return args.Get(0).(bool) -} - -func (m *MockDb) SetLanguage(language *lang.Language) { - m.Called(language) -} - -func (m *MockDb) SetLock(uint8, bool) error { - args := m.Called() - return args.Error(0) -} - -func (m *MockDb) Connect(ctx context.Context, connectionStr string) error { - args := m.Called(ctx, connectionStr) - return args.Error(0) -} - -func (m *MockDb) SetSession(sessionId string) { - m.Called(sessionId) -} - -func (m *MockDb) Put(ctx context.Context, key, value []byte) error { - args := m.Called(ctx, key, value) - return args.Error(0) -} - -func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) { - args := m.Called(ctx, key) - return nil, args.Error(0) -} - -func (m *MockDb) Close() error { - args := m.Called(nil) - return args.Error(0) -} diff --git a/internal/mocks/servicemock.go b/internal/mocks/servicemock.go deleted file mode 100644 index 9fb6d3e..0000000 --- a/internal/mocks/servicemock.go +++ /dev/null @@ -1,26 +0,0 @@ -package mocks - -import ( - "git.grassecon.net/urdt/ussd/internal/models" - "github.com/stretchr/testify/mock" -) - -// MockAccountService implements AccountServiceInterface for testing -type MockAccountService struct { - mock.Mock -} - -func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) { - args := m.Called() - return args.Get(0).(*models.AccountResponse), args.Error(1) -} - -func (m *MockAccountService) CheckBalance(publicKey string) (string, error) { - args := m.Called(publicKey) - return args.String(0), args.Error(1) -} - -func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) { - args := m.Called(trackingId) - return args.String(0), args.Error(1) -} \ No newline at end of file diff --git a/internal/mocks/userdbmock.go b/internal/mocks/userdbmock.go deleted file mode 100644 index ff3f18d..0000000 --- a/internal/mocks/userdbmock.go +++ /dev/null @@ -1,24 +0,0 @@ -package mocks - -import ( - "context" - - "git.defalsify.org/vise.git/db" - "git.grassecon.net/urdt/ussd/internal/utils" - "github.com/stretchr/testify/mock" -) - -type MockUserDataStore struct { - db.Db - mock.Mock -} - -func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) { - args := m.Called(ctx, sessionId, typ) - return args.Get(0).([]byte), args.Error(1) -} - -func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error { - args := m.Called(ctx, sessionId, typ, value) - return args.Error(0) -} diff --git a/internal/models/accountresponse.go b/internal/models/accountresponse.go deleted file mode 100644 index 1422a20..0000000 --- a/internal/models/accountresponse.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import ( - "encoding/json" - -) - -type AccountResponse struct { - Ok bool `json:"ok"` - Result struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - } `json:"result"` -} \ No newline at end of file diff --git a/internal/models/balanceresponse.go b/internal/models/balanceresponse.go deleted file mode 100644 index 57c8e5a..0000000 --- a/internal/models/balanceresponse.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import "encoding/json" - - -type BalanceResponse struct { - Ok bool `json:"ok"` - Result struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - } `json:"result"` -} diff --git a/internal/models/trackstatusresponse.go b/internal/models/trackstatusresponse.go deleted file mode 100644 index 6054281..0000000 --- a/internal/models/trackstatusresponse.go +++ /dev/null @@ -1,20 +0,0 @@ -package models - -import ( - "encoding/json" - "time" -) - - -type TrackStatusResponse struct { - Ok bool `json:"ok"` - Result struct { - Transaction struct { - CreatedAt time.Time `json:"createdAt"` - Status string `json:"status"` - TransferValue json.Number `json:"transferValue"` - TxHash string `json:"txHash"` - TxType string `json:"txType"` - } - } `json:"result"` -} \ No newline at end of file diff --git a/internal/storage/db.go b/internal/storage/db.go index b2ac6a9..8c9ff35 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -10,34 +10,38 @@ const ( DATATYPE_USERSUB = 64 ) +// PrefixDb interface abstracts the database operations. +type PrefixDb interface { + Get(ctx context.Context, key []byte) ([]byte, error) + Put(ctx context.Context, key []byte, val []byte) error +} + +var _ PrefixDb = (*SubPrefixDb)(nil) + type SubPrefixDb struct { store db.Db - pfx []byte + pfx []byte } func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { return &SubPrefixDb{ store: store, - pfx: pfx, + pfx: pfx, } } -func(s *SubPrefixDb) toKey(k []byte) []byte { - return append(s.pfx, k...) +func (s *SubPrefixDb) toKey(k []byte) []byte { + return append(s.pfx, k...) } -func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { - s.store.SetPrefix(DATATYPE_USERSUB) +func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { + s.store.SetPrefix(DATATYPE_USERSUB) key = s.toKey(key) - v, err := s.store.Get(ctx, key) - if err != nil { - return nil, err - } - return v, nil + return s.store.Get(ctx, key) } -func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { - s.store.SetPrefix(DATATYPE_USERSUB) +func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { + s.store.SetPrefix(DATATYPE_USERSUB) key = s.toKey(key) - return s.store.Put(ctx, key, val) + return s.store.Put(ctx, key, val) } diff --git a/internal/storage/gdbm.go b/internal/storage/gdbm.go index eb959cf..49de570 100644 --- a/internal/storage/gdbm.go +++ b/internal/storage/gdbm.go @@ -109,6 +109,7 @@ func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) { func(tdb *ThreadGdbmDb) Close() error { tdb.reserve() close(dbC[tdb.connStr]) + delete(dbC, tdb.connStr) err := tdb.db.Close() tdb.db = nil return err diff --git a/internal/storage/storageservice.go b/internal/storage/storageservice.go index 07bccd6..9fa1839 100644 --- a/internal/storage/storageservice.go +++ b/internal/storage/storageservice.go @@ -8,14 +8,16 @@ import ( "git.defalsify.org/vise.git/db" fsdb "git.defalsify.org/vise.git/db/fs" + "git.defalsify.org/vise.git/db/postgres" + "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" - "git.defalsify.org/vise.git/logging" + "git.grassecon.net/urdt/ussd/initializers" ) var ( logg = logging.NewVanilla().WithDomain("storage") -) +) type StorageService interface { GetPersister(ctx context.Context) (*persist.Persister, error) @@ -24,40 +26,86 @@ type StorageService interface { EnsureDbDir() error } -type MenuStorageService struct{ - dbDir string - resourceDir string +type MenuStorageService struct { + dbDir string + resourceDir string resourceStore db.Db - stateStore db.Db + stateStore db.Db userDataStore db.Db } +func buildConnStr() string { + host := initializers.GetEnv("DB_HOST", "localhost") + user := initializers.GetEnv("DB_USER", "postgres") + password := initializers.GetEnv("DB_PASSWORD", "") + dbName := initializers.GetEnv("DB_NAME", "") + port := initializers.GetEnv("DB_PORT", "5432") + + return fmt.Sprintf( + "postgres://%s:%s@%s:%s/%s", + user, password, host, port, dbName, + ) +} + func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { return &MenuStorageService{ - dbDir: dbDir, + dbDir: dbDir, resourceDir: resourceDir, } } -func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { - ms.stateStore = NewThreadGdbmDb() - storeFile := path.Join(ms.dbDir, "state.gdbm") - err := ms.stateStore.Connect(ctx, storeFile) +func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) { + database, ok := ctx.Value("Database").(string) + if !ok { + return nil, fmt.Errorf("failed to select the database") + } + + if existingDb != nil { + return existingDb, nil + } + + var newDb db.Db + var err error + + if database == "postgres" { + newDb = postgres.NewPgDb() + connStr := buildConnStr() + err = newDb.Connect(ctx, connStr) + } else { + newDb = NewThreadGdbmDb() + storeFile := path.Join(ms.dbDir, fileName) + err = newDb.Connect(ctx, storeFile) + } + if err != nil { return nil, err } - pr := persist.NewPersister(ms.stateStore) - logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", ms.stateStore) + + return newDb, nil +} + +func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { + stateStore, err := ms.GetStateStore(ctx) + if err != nil { + return nil, err + } + + pr := persist.NewPersister(stateStore) + logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", stateStore) return pr, nil } func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { - ms.userDataStore = NewThreadGdbmDb() - storeFile := path.Join(ms.dbDir, "userdata.gdbm") - err := ms.userDataStore.Connect(ctx, storeFile) + if ms.userDataStore != nil { + return ms.userDataStore, nil + } + + userDataStore, err := ms.getOrCreateDb(ctx, ms.userDataStore, "userdata.gdbm") if err != nil { return nil, err } + + ms.userDataStore = userDataStore return ms.userDataStore, nil } @@ -73,14 +121,15 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) { if ms.stateStore != nil { - panic("set up store when already exists") + return ms.stateStore, nil } - ms.stateStore = NewThreadGdbmDb() - storeFile := path.Join(ms.dbDir, "state.gdbm") - err := ms.stateStore.Connect(ctx, storeFile) + + stateStore, err := ms.getOrCreateDb(ctx, ms.stateStore, "state.gdbm") if err != nil { return nil, err } + + ms.stateStore = stateStore return ms.stateStore, nil } diff --git a/internal/testutil/TestEngine.go b/internal/testutil/TestEngine.go new file mode 100644 index 0000000..3fcb307 --- /dev/null +++ b/internal/testutil/TestEngine.go @@ -0,0 +1,124 @@ +package testutil + +import ( + "context" + "fmt" + "os" + "path" + "time" + + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/logging" + "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/internal/handlers" + "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/internal/testutil/testservice" + "git.grassecon.net/urdt/ussd/internal/testutil/testtag" + testdataloader "github.com/peteole/testdata-loader" + "git.grassecon.net/urdt/ussd/remote" +) + +var ( + baseDir = testdataloader.GetBasePath() + logg = logging.NewVanilla() + scriptDir = path.Join(baseDir, "services", "registration") +) + +func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { + ctx := context.Background() + ctx = context.WithValue(ctx, "SessionId", sessionId) + ctx = context.WithValue(ctx, "Database", "gdbm") + pfp := path.Join(scriptDir, "pp.csv") + + var eventChannel = make(chan bool) + + cfg := engine.Config{ + Root: "root", + SessionId: sessionId, + OutputSize: uint32(160), + FlagCount: uint32(128), + } + + dbDir := ".test_state" + resourceDir := scriptDir + menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + + err := menuStorageService.EnsureDbDir() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + rs, err := menuStorageService.GetResource(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + pe, err := menuStorageService.GetPersister(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + userDataStore, err := menuStorageService.GetUserdataDb(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + dbResource, ok := rs.(*resource.DbResource) + if !ok { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) + lhs.SetDataStore(&userDataStore) + lhs.SetPersister(pe) + + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + if testtag.AccountService == nil { + testtag.AccountService = &remote.AccountService{} + } + + switch testtag.AccountService.(type) { + case *testservice.TestAccountService: + go func() { + eventChannel <- false + }() + case *remote.AccountService: + go func() { + time.Sleep(5 * time.Second) // Wait for 5 seconds + eventChannel <- true + }() + default: + panic("Unknown account service type") + } + + hl, err := lhs.GetHandler(testtag.AccountService) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + en := lhs.GetEngine() + en = en.WithFirst(hl.Init) + cleanFn := func() { + err := en.Finish() + if err != nil { + logg.Errorf(err.Error()) + } + + err = menuStorageService.Close() + if err != nil { + logg.Errorf(err.Error()) + } + logg.Infof("testengine storage closed") + } + return en, cleanFn, eventChannel +} diff --git a/internal/testutil/driver/groupdriver.go b/internal/testutil/driver/groupdriver.go new file mode 100644 index 0000000..68cb7e3 --- /dev/null +++ b/internal/testutil/driver/groupdriver.go @@ -0,0 +1,111 @@ +package driver + +import ( + "encoding/json" + "log" + "os" + "regexp" +) + +type Step struct { + Input string `json:"input"` + ExpectedContent string `json:"expectedContent"` +} + +func (s *Step) MatchesExpectedContent(content []byte) (bool, error) { + pattern := regexp.QuoteMeta(s.ExpectedContent) + re, err := regexp.Compile(pattern) + if err != nil { + return false, err + } + if re.Match([]byte(content)) { + return true, nil + } + return false, nil +} + +// Group represents a group of steps +type Group struct { + Name string `json:"name"` + Steps []Step `json:"steps"` +} + +type TestCase struct { + Name string + Input string + ExpectedContent string +} + +func (s *TestCase) MatchesExpectedContent(content []byte) (bool, error) { + pattern := regexp.QuoteMeta(s.ExpectedContent) + re, err := regexp.Compile(pattern) + if err != nil { + return false, err + } + // Check if the content matches the regex pattern + if re.Match(content) { + return true, nil + } + return false, nil +} + +// DataGroup represents the overall structure of the JSON. +type DataGroup struct { + Groups []Group `json:"groups"` +} + +type Session struct { + Name string `json:"name"` + Groups []Group `json:"groups"` +} + +func ReadData() []Session { + data, err := os.ReadFile("test_setup.json") + if err != nil { + log.Fatalf("Failed to read file: %v", err) + } + // Unmarshal JSON data + var sessions []Session + err = json.Unmarshal(data, &sessions) + if err != nil { + log.Fatalf("Failed to unmarshal JSON: %v", err) + } + + return sessions +} + +func FilterGroupsByName(groups []Group, name string) []Group { + var filteredGroups []Group + for _, group := range groups { + if group.Name == name { + filteredGroups = append(filteredGroups, group) + } + } + return filteredGroups +} + +func LoadTestGroups(filePath string) (DataGroup, error) { + var sessionsData DataGroup + data, err := os.ReadFile(filePath) + if err != nil { + return sessionsData, err + } + err = json.Unmarshal(data, &sessionsData) + return sessionsData, err +} + +func CreateTestCases(group DataGroup) []TestCase { + var tests []TestCase + for _, group := range group.Groups { + for _, step := range group.Steps { + // Create a test case for each group + tests = append(tests, TestCase{ + Name: group.Name, + Input: step.Input, + ExpectedContent: step.ExpectedContent, + }) + } + } + + return tests +} diff --git a/internal/mocks/httpmocks/enginemock.go b/internal/testutil/mocks/httpmocks/enginemock.go similarity index 100% rename from internal/mocks/httpmocks/enginemock.go rename to internal/testutil/mocks/httpmocks/enginemock.go diff --git a/internal/mocks/httpmocks/requesthandlermock.go b/internal/testutil/mocks/httpmocks/requesthandlermock.go similarity index 100% rename from internal/mocks/httpmocks/requesthandlermock.go rename to internal/testutil/mocks/httpmocks/requesthandlermock.go diff --git a/internal/mocks/httpmocks/requestparsermock.go b/internal/testutil/mocks/httpmocks/requestparsermock.go similarity index 100% rename from internal/mocks/httpmocks/requestparsermock.go rename to internal/testutil/mocks/httpmocks/requestparsermock.go diff --git a/internal/mocks/httpmocks/writermock.go b/internal/testutil/mocks/httpmocks/writermock.go similarity index 100% rename from internal/mocks/httpmocks/writermock.go rename to internal/testutil/mocks/httpmocks/writermock.go diff --git a/internal/testutil/mocks/servicemock.go b/internal/testutil/mocks/servicemock.go new file mode 100644 index 0000000..76803ba --- /dev/null +++ b/internal/testutil/mocks/servicemock.go @@ -0,0 +1,45 @@ +package mocks + +import ( + "context" + + "git.grassecon.net/urdt/ussd/models" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" + "github.com/stretchr/testify/mock" +) + +// MockAccountService implements AccountServiceInterface for testing +type MockAccountService struct { + mock.Mock +} + +func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { + args := m.Called() + return args.Get(0).(*models.AccountResult), args.Error(1) +} + +func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { + args := m.Called(publicKey) + return args.Get(0).(*models.BalanceResult), args.Error(1) +} + +func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) { + args := m.Called(trackingId) + return args.Get(0).(*models.TrackStatusResult), args.Error(1) +} + + +func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + args := m.Called(publicKey) + return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) +} + +func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { + args := m.Called(publicKey) + return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1) +} + +func(m MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { + args := m.Called(address) + return args.Get(0).(*models.VoucherDataResult), args.Error(1) +} diff --git a/internal/testutil/testservice/TestAccountService.go b/internal/testutil/testservice/TestAccountService.go new file mode 100644 index 0000000..8752d6f --- /dev/null +++ b/internal/testutil/testservice/TestAccountService.go @@ -0,0 +1,52 @@ +package testservice + +import ( + "context" + "encoding/json" + + "git.grassecon.net/urdt/ussd/models" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +type TestAccountService struct { +} + +func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { + return &models.AccountResult { + TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", + PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", + }, nil +} + +func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { + balanceResponse := &models.BalanceResult { + Balance: "0.003 CELO", + Nonce: json.Number("0"), + } + return balanceResponse, nil +} + +func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { + return &models.TrackStatusResult { + Active: true, + }, nil +} + +func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + return []dataserviceapi.TokenHoldings { + dataserviceapi.TokenHoldings { + ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", + TokenSymbol: "SRF", + TokenDecimals: "6", + Balance: "2745987", + }, + }, nil +} + +func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { + return []dataserviceapi.Last10TxResponse{}, nil +} + +func(m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { + return &models.VoucherDataResult{}, nil +} diff --git a/internal/testutil/testtag/offlinetest.go b/internal/testutil/testtag/offlinetest.go new file mode 100644 index 0000000..831bf09 --- /dev/null +++ b/internal/testutil/testtag/offlinetest.go @@ -0,0 +1,12 @@ +// +build !online + +package testtag + +import ( + "git.grassecon.net/urdt/ussd/remote" + accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice" +) + +var ( + AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{} +) diff --git a/internal/testutil/testtag/onlinetest.go b/internal/testutil/testtag/onlinetest.go new file mode 100644 index 0000000..92cbb14 --- /dev/null +++ b/internal/testutil/testtag/onlinetest.go @@ -0,0 +1,9 @@ +// +build online + +package testtag + +import "git.grassecon.net/urdt/ussd/internal/handlers/server" + +var ( + AccountService server.AccountServiceInterface +) diff --git a/internal/utils/adminstore.go b/internal/utils/adminstore.go new file mode 100644 index 0000000..a492479 --- /dev/null +++ b/internal/utils/adminstore.go @@ -0,0 +1,51 @@ +package utils + +import ( + "context" + + "git.defalsify.org/vise.git/db" + fsdb "git.defalsify.org/vise.git/db/fs" + "git.defalsify.org/vise.git/logging" +) + +var ( + logg = logging.NewVanilla().WithDomain("adminstore") +) + +type AdminStore struct { + ctx context.Context + FsStore db.Db +} + +func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) { + fsStore, err := getFsStore(ctx, fileName) + if err != nil { + return nil, err + } + return &AdminStore{ctx: ctx, FsStore: fsStore}, nil +} + +func getFsStore(ctx context.Context, connectStr string) (db.Db, error) { + fsStore := fsdb.NewFsDb() + err := fsStore.Connect(ctx, connectStr) + fsStore.SetPrefix(db.DATATYPE_USERDATA) + if err != nil { + return nil, err + } + return fsStore, nil +} + +// Checks if the given sessionId is listed as an admin. +func (as *AdminStore) IsAdmin(sessionId string) (bool, error) { + _, err := as.FsStore.Get(as.ctx, []byte(sessionId)) + if err != nil { + if db.IsNotFound(err) { + logg.Printf(logging.LVL_INFO, "Returning false because session id was not found") + return false, nil + } else { + return false, err + } + } + + return true, nil +} diff --git a/menutraversal_test/group_test.json b/menutraversal_test/group_test.json new file mode 100644 index 0000000..a219a6c --- /dev/null +++ b/menutraversal_test/group_test.json @@ -0,0 +1,460 @@ +{ + "groups": [ + { + "name": "my_account_change_pin", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "5", + "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your old PIN\n\n0:Back" + }, + { + "input": "1234", + "expectedContent": "Enter a new four number PIN:\n\n0:Back" + }, + { + "input": "1234", + "expectedContent": "Confirm your new PIN:\n\n0:Back" + }, + { + "input": "1234", + "expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_language_change", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "2", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1235", + "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Select language:\n0:english\n1:kiswahili" + }, + { + "input": "0", + "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_check_my_balance", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "3", + "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" + }, + { + "input": "1", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1235", + "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" + + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_check_community_balance", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "3", + "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" + }, + { + "input": "2", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1235", + "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" + + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_edit_firstname", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your first names:\n0:Back" + }, + { + "input": "foo", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_edit_familyname", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "2", + "expectedContent": "Enter family name:\n0:Back" + }, + { + "input": "bar", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + + ] + }, + { + "name": "menu_my_account_edit_gender", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "3", + "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" + }, + { + "input": "1", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_edit_yob", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "4", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1945", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_edit_location", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "5", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_edit_offerings", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "6", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + }, + { + "name": "menu_my_account_view_profile", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "7", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + + + + + + diff --git a/menutraversal_test/menu_traversal_test.go b/menutraversal_test/menu_traversal_test.go new file mode 100644 index 0000000..d79b771 --- /dev/null +++ b/menutraversal_test/menu_traversal_test.go @@ -0,0 +1,380 @@ +package menutraversaltest + +import ( + "bytes" + "context" + "log" + "math/rand" + "os" + "regexp" + "testing" + + "git.grassecon.net/urdt/ussd/internal/testutil" + "git.grassecon.net/urdt/ussd/internal/testutil/driver" + "github.com/gofrs/uuid" +) + +var ( + testData = driver.ReadData() + testStore = ".test_state" + groupTestFile = "group_test.json" + sessionID string + src = rand.NewSource(42) + g = rand.New(src) +) + +func GenerateSessionId() string { + uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) + v, err := uu.NewV4() + if err != nil { + panic(err) + } + return v.String() +} + +// Extract the public key from the engine response +func extractPublicKey(response []byte) string { + // Regex pattern to match the public key starting with 0x and 40 characters + re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`) + match := re.Find(response) + if match != nil { + return string(match) + } + return "" +} + +// Extracts the balance value from the engine response. +func extractBalance(response []byte) string { + // Regex to match "Balance: " followed by a newline + re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`) + match := re.FindSubmatch(response) + if match != nil { + return string(match[1]) + " " + string(match[3]) // " " + } + return "" +} + +// Extracts the Maximum amount value from the engine response. +func extractMaxAmount(response []byte) string { + // Regex to match "Maximum amount: " followed by a newline + re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`) + match := re.FindSubmatch(response) + if match != nil { + return string(match[1]) // "" + } + return "" +} + +// Extracts the send amount value from the engine response. +func extractSendAmount(response []byte) string { + // Regex to match the pattern "will receive X.XX SYM from" + re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`) + match := re.FindSubmatch(response) + if match != nil { + return string(match[1]) // Returns "X.XX SYM" + } + return "" +} + +func TestMain(m *testing.M) { + sessionID = GenerateSessionId() + defer func() { + if err := os.RemoveAll(testStore); err != nil { + log.Fatalf("Failed to delete state store %s: %v", testStore, err) + } + }() + m.Run() +} + +func TestAccountCreationSuccessful(t *testing.T) { + en, fn, eventChannel := testutil.TestEngine(sessionID) + defer fn() + ctx := context.Background() + sessions := testData + for _, session := range sessions { + groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful") + for _, group := range groups { + for _, step := range group.Steps { + cont, err := en.Exec(ctx, []byte(step.Input)) + if err != nil { + t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) + } + if !cont { + break + } + w := bytes.NewBuffer(nil) + _, err = en.Flush(ctx, w) + if err != nil { + t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) + } + b := w.Bytes() + match, err := step.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) + } + } + } + } + <-eventChannel + +} + +func TestAccountRegistrationRejectTerms(t *testing.T) { + // Generate a new UUID for this edge case test + uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) + v, err := uu.NewV4() + if err != nil { + t.Fail() + } + edgeCaseSessionID := v.String() + en, fn, _ := testutil.TestEngine(edgeCaseSessionID) + defer fn() + ctx := context.Background() + sessions := testData + for _, session := range sessions { + groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms") + for _, group := range groups { + for _, step := range group.Steps { + cont, err := en.Exec(ctx, []byte(step.Input)) + if err != nil { + t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) + return + } + if !cont { + break + } + w := bytes.NewBuffer(nil) + if _, err := en.Flush(ctx, w); err != nil { + t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) + } + + b := w.Bytes() + match, err := step.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) + } + } + } + } +} + +func TestMainMenuHelp(t *testing.T) { + en, fn, _ := testutil.TestEngine(sessionID) + defer fn() + ctx := context.Background() + sessions := testData + for _, session := range sessions { + groups := driver.FilterGroupsByName(session.Groups, "main_menu_help") + for _, group := range groups { + for _, step := range group.Steps { + cont, err := en.Exec(ctx, []byte(step.Input)) + if err != nil { + t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) + return + } + if !cont { + break + } + w := bytes.NewBuffer(nil) + if _, err := en.Flush(ctx, w); err != nil { + t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) + } + + b := w.Bytes() + balance := extractBalance(b) + + expectedContent := []byte(step.ExpectedContent) + expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) + + step.ExpectedContent = string(expectedContent) + match, err := step.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) + } + } + } + } +} + +func TestMainMenuQuit(t *testing.T) { + en, fn, _ := testutil.TestEngine(sessionID) + defer fn() + ctx := context.Background() + sessions := testData + for _, session := range sessions { + groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit") + for _, group := range groups { + for _, step := range group.Steps { + cont, err := en.Exec(ctx, []byte(step.Input)) + if err != nil { + t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) + return + } + if !cont { + break + } + w := bytes.NewBuffer(nil) + if _, err := en.Flush(ctx, w); err != nil { + t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) + } + + b := w.Bytes() + balance := extractBalance(b) + + expectedContent := []byte(step.ExpectedContent) + expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) + + step.ExpectedContent = string(expectedContent) + match, err := step.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) + } + } + } + } +} + +func TestMyAccount_MyAddress(t *testing.T) { + en, fn, _ := testutil.TestEngine(sessionID) + defer fn() + ctx := context.Background() + sessions := testData + for _, session := range sessions { + groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address") + for _, group := range groups { + for index, step := range group.Steps { + t.Logf("step %v with input %v", index, step.Input) + cont, err := en.Exec(ctx, []byte(step.Input)) + if err != nil { + t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) + return + } + if !cont { + break + } + w := bytes.NewBuffer(nil) + if _, err := en.Flush(ctx, w); err != nil { + t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err) + } + b := w.Bytes() + + balance := extractBalance(b) + publicKey := extractPublicKey(b) + + expectedContent := []byte(step.ExpectedContent) + expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) + expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1) + + step.ExpectedContent = string(expectedContent) + match, err := step.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b) + } + } + } + } +} + +func TestMainMenuSend(t *testing.T) { + en, fn, _ := testutil.TestEngine(sessionID) + defer fn() + ctx := context.Background() + sessions := testData + for _, session := range sessions { + groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs") + for _, group := range groups { + for _, step := range group.Steps { + cont, err := en.Exec(ctx, []byte(step.Input)) + if err != nil { + t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) + return + } + if !cont { + break + } + w := bytes.NewBuffer(nil) + if _, err := en.Flush(ctx, w); err != nil { + t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) + } + + b := w.Bytes() + balance := extractBalance(b) + max_amount := extractMaxAmount(b) + send_amount := extractSendAmount(b) + + expectedContent := []byte(step.ExpectedContent) + expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) + expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1) + expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1) + expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1) + + step.ExpectedContent = string(expectedContent) + match, err := step.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) + } + } + } + } +} + +func TestGroups(t *testing.T) { + groups, err := driver.LoadTestGroups(groupTestFile) + if err != nil { + log.Fatalf("Failed to load test groups: %v", err) + } + en, fn, _ := testutil.TestEngine(sessionID) + defer fn() + ctx := context.Background() + // Create test cases from loaded groups + tests := driver.CreateTestCases(groups) + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + cont, err := en.Exec(ctx, []byte(tt.Input)) + if err != nil { + t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err) + return + } + if !cont { + return + } + w := bytes.NewBuffer(nil) + if _, err := en.Flush(ctx, w); err != nil { + t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err) + } + b := w.Bytes() + balance := extractBalance(b) + + expectedContent := []byte(tt.ExpectedContent) + expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) + + tt.ExpectedContent = string(expectedContent) + + match, err := tt.MatchesExpectedContent(b) + if err != nil { + t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err) + } + if !match { + t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b) + } + }) + } +} diff --git a/menutraversal_test/test_setup.json b/menutraversal_test/test_setup.json new file mode 100644 index 0000000..13166a4 --- /dev/null +++ b/menutraversal_test/test_setup.json @@ -0,0 +1,153 @@ +[ + { + "name": "session one", + "groups": [ + { + "name": "account_creation_successful", + "steps": [ + { + "input": "", + "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" + }, + { + "input": "0", + "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" + }, + { + "input": "0", + "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit" + }, + { + "input": "1234", + "expectedContent": "Enter your four number PIN again:" + }, + { + "input": "1111", + "expectedContent": "The PIN is not a match. Try again\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "Enter your four number PIN again:" + }, + { + "input": "1234", + "expectedContent": "Your account is being created...Thank you for using Sarafu. Goodbye!" + } + ] + }, + { + "name": "account_creation_reject_terms", + "steps": [ + { + "input": "", + "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" + }, + { + "input": "0", + "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" + }, + { + "input": "1", + "expectedContent": "Thank you for using Sarafu. Goodbye!" + } + ] + }, + { + "name": "send_with_invalid_inputs", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "1", + "expectedContent": "Enter recipient's phone number:\n0:Back" + }, + { + "input": "000", + "expectedContent": "000 is not registered or invalid, please try again:\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "Enter recipient's phone number:\n0:Back" + }, + { + "input": "065656", + "expectedContent": "{max_amount}\nEnter amount:\n0:Back" + }, + { + "input": "10000000", + "expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "{max_amount}\nEnter amount:\n0:Back" + }, + { + "input": "1.00", + "expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" + }, + { + "input": "1222", + "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + }, + { + "input": "1", + "expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" + }, + { + "input": "1234", + "expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}." + } + ] + }, + { + "name": "main_menu_help", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "4", + "expectedContent": "For more help,please call: 0757628885" + } + ] + }, + { + "name": "main_menu_quit", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "9", + "expectedContent": "Thank you for using Sarafu. Goodbye!" + } + ] + }, + { + "name": "menu_my_account_my_address", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "6", + "expectedContent": "Address: {public_key}\n9:Quit" + }, + { + "input": "9", + "expectedContent": "Thank you for using Sarafu. Goodbye!" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/models/accountresponse.go b/models/accountresponse.go new file mode 100644 index 0000000..278e0e9 --- /dev/null +++ b/models/accountresponse.go @@ -0,0 +1,6 @@ +package models + +type AccountResult struct { + PublicKey string `json:"publicKey"` + TrackingId string `json:"trackingId"` +} diff --git a/models/balanceresponse.go b/models/balanceresponse.go new file mode 100644 index 0000000..b2baa41 --- /dev/null +++ b/models/balanceresponse.go @@ -0,0 +1,9 @@ +package models + +import "encoding/json" + + +type BalanceResult struct { + Balance string `json:"balance"` + Nonce json.Number `json:"nonce"` +} diff --git a/models/tokenresponse.go b/models/tokenresponse.go new file mode 100644 index 0000000..d243d93 --- /dev/null +++ b/models/tokenresponse.go @@ -0,0 +1,18 @@ +package models + +type ApiResponse struct { + OK bool `json:"ok"` + Description string `json:"description"` + Result Result `json:"result"` +} + +type Result struct { + Holdings []Holding `json:"holdings"` +} + +type Holding struct { + ContractAddress string `json:"contractAddress"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimals string `json:"tokenDecimals"` + Balance string `json:"balance"` +} diff --git a/models/trackstatusresponse.go b/models/trackstatusresponse.go new file mode 100644 index 0000000..47d757d --- /dev/null +++ b/models/trackstatusresponse.go @@ -0,0 +1,18 @@ +package models + +import ( + "encoding/json" + "time" +) + +type Transaction struct { + CreatedAt time.Time `json:"createdAt"` + Status string `json:"status"` + TransferValue json.Number `json:"transferValue"` + TxHash string `json:"txHash"` + TxType string `json:"txType"` +} + +type TrackStatusResult struct { + Active bool `json:"active"` +} diff --git a/models/vouchersresponse.go b/models/vouchersresponse.go new file mode 100644 index 0000000..8cf3ec6 --- /dev/null +++ b/models/vouchersresponse.go @@ -0,0 +1,21 @@ +package models + +import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" + +//type VoucherHoldingResponse struct { +// Ok bool `json:"ok"` +// Description string `json:"description"` +// Result VoucherResult `json:"result"` +//} + +// VoucherResult holds the list of token holdings +type VoucherResult struct { + Holdings []dataserviceapi.TokenHoldings `json:"holdings"` +} + +type VoucherDataResult struct { + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimals string `json:"tokenDecimals"` + SinkAddress string `json:"sinkAddress"` +} diff --git a/remote/accountservice.go b/remote/accountservice.go new file mode 100644 index 0000000..73052f6 --- /dev/null +++ b/remote/accountservice.go @@ -0,0 +1,224 @@ +package remote + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" + "github.com/grassrootseconomics/eth-custodial/pkg/api" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/models" +) + +var ( +) + +type AccountServiceInterface interface { + CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) + CreateAccount(ctx context.Context) (*models.AccountResult, error) + TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) + FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) + FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) + VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) +} + +type AccountService struct { +} + +// Parameters: +// - trackingId: A unique identifier for the account.This should be obtained from a previous call to +// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the +// AccountResponse struct can be used here to check the account status during a transaction. +// +// Returns: +// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string. +// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. +// If no error occurs, this will be nil +func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { + var r models.TrackStatusResult + + ep, err := url.JoinPath(config.TrackURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doCustodialRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. +// Parameters: +// - publicKey: The public key associated with the account whose balance needs to be checked. +func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { + var balanceResult models.BalanceResult + + ep, err := url.JoinPath(config.BalanceURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doCustodialRequest(ctx, req, &balanceResult) + return &balanceResult, err +} + + +// CreateAccount creates a new account in the custodial system. +// Returns: +// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account. +// If there is an error during the request or processing, this will be nil. +// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. +// If no error occurs, this will be nil. +func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { + var r models.AccountResult + // Create a new request + req, err := http.NewRequest("POST", config.CreateAccountURL, nil) + if err != nil { + return nil, err + } + + _, err = doCustodialRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint +// Parameters: +// - publicKey: The public key associated with the account. +func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + var r []dataserviceapi.TokenHoldings + + ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doDataRequest(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + + +// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint +// Parameters: +// - publicKey: The public key associated with the account. +func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { + var r []dataserviceapi.Last10TxResponse + + ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doDataRequest(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + + +// VoucherData retrieves voucher metadata from the data indexer API endpoint. +// Parameters: +// - address: The voucher address. +func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { + var voucherDataResult models.VoucherDataResult + + ep, err := url.JoinPath(config.VoucherDataURL, address) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doCustodialRequest(ctx, req, &voucherDataResult) + return &voucherDataResult, err +} + +func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { + var okResponse api.OKResponse + var errResponse api.ErrResponse + + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + errResponse.Description = err.Error() + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode >= http.StatusBadRequest { + err := json.Unmarshal([]byte(body), &errResponse) + if err != nil { + return nil, err + } + return nil, errors.New(errResponse.Description) + } + err = json.Unmarshal([]byte(body), &okResponse) + if err != nil { + return nil, err + } + if len(okResponse.Result) == 0 { + return nil, errors.New("Empty api result") + } + return &okResponse, nil + + v, err := json.Marshal(okResponse.Result) + if err != nil { + return nil, err + } + + err = json.Unmarshal(v, &rcpt) + return &okResponse, err +} + +func doCustodialRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { + req.Header.Set("X-GE-KEY", config.CustodialAPIKey) + return doRequest(ctx, req, rcpt) +} + +func doDataRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { + req.Header.Set("X-GE-KEY", config.DataAPIKey) + return doRequest(ctx, req, rcpt) +} diff --git a/sample_tokens.json b/sample_tokens.json new file mode 100644 index 0000000..07126ed --- /dev/null +++ b/sample_tokens.json @@ -0,0 +1,44 @@ +{ + "ok": true, + "description": "Token holdings with current balances", + "result": { + "holdings": [ + { + "contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", + "tokenSymbol": "FSPTST", + "tokenDecimals": "6", + "balance": "8869964242" + }, + { + "contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA", + "tokenSymbol": "GEO", + "tokenDecimals": "6", + "balance": "9884" + }, + { + "contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273", + "tokenSymbol": "MFNK", + "tokenDecimals": "6", + "balance": "19788697" + }, + { + "contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83", + "tokenSymbol": "MILO", + "tokenDecimals": "6", + "balance": "75" + }, + { + "contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + "tokenSymbol": "SOHAIL", + "tokenDecimals": "6", + "balance": "27874115" + }, + { + "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", + "tokenSymbol": "SRF", + "tokenDecimals": "6", + "balance": "2745987" + } + ] + } + } diff --git a/services/registration/account_creation.vis b/services/registration/account_creation.vis index f4f326b..380fe6d 100644 --- a/services/registration/account_creation.vis +++ b/services/registration/account_creation.vis @@ -1,4 +1,4 @@ -RELOAD verify_pin +RELOAD verify_create_pin CATCH create_pin_mismatch flag_pin_mismatch 1 LOAD quit 0 HALT diff --git a/services/registration/amount.vis b/services/registration/amount.vis index b491fab..82e1fd4 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -1,13 +1,15 @@ LOAD reset_transaction_amount 0 LOAD max_amount 10 +RELOAD max_amount MAP max_amount MOUT back 0 HALT LOAD validate_amount 64 RELOAD validate_amount +CATCH api_failure flag_api_call_error 1 CATCH invalid_amount flag_invalid_amount 1 INCMP _ 0 LOAD get_recipient 12 LOAD get_sender 64 -LOAD get_amount 12 +LOAD get_amount 32 INCMP transaction_pin * diff --git a/services/registration/api_failure b/services/registration/api_failure new file mode 100644 index 0000000..06d2d9e --- /dev/null +++ b/services/registration/api_failure @@ -0,0 +1 @@ +Failed to connect to the custodial service.Please try again. \ No newline at end of file diff --git a/services/registration/api_failure.vis b/services/registration/api_failure.vis new file mode 100644 index 0000000..e045355 --- /dev/null +++ b/services/registration/api_failure.vis @@ -0,0 +1,5 @@ +MOUT retry 0 +MOUT quit 9 +HALT +INCMP _ 0 +INCMP quit 9 diff --git a/services/registration/balances.vis b/services/registration/balances.vis index dd14501..aef397f 100644 --- a/services/registration/balances.vis +++ b/services/registration/balances.vis @@ -1,4 +1,5 @@ LOAD reset_account_authorized 0 +RELOAD reset_account_authorized MOUT my_balance 1 MOUT community_balance 2 MOUT back 0 diff --git a/services/registration/community_balance b/services/registration/community_balance index 5d292ee..f8f8318 100644 --- a/services/registration/community_balance +++ b/services/registration/community_balance @@ -1 +1 @@ -Your community balance is: 0.00SRF \ No newline at end of file +{{.fetch_custodial_balances}} \ No newline at end of file diff --git a/services/registration/community_balance.vis b/services/registration/community_balance.vis index 151c6d8..85ae93a 100644 --- a/services/registration/community_balance.vis +++ b/services/registration/community_balance.vis @@ -1,5 +1,11 @@ -LOAD reset_incorrect 0 +LOAD reset_incorrect 6 +LOAD fetch_custodial_balances 0 +CATCH api_failure flag_api_call_error 1 +MAP fetch_custodial_balances CATCH incorrect_pin flag_incorrect_pin 1 CATCH pin_entry flag_account_authorized 0 -LOAD quit_with_balance 0 +MOUT back 0 +MOUT quit 9 HALT +INCMP _ 0 +INCMP quit 9 diff --git a/services/registration/confirm_create_pin.vis b/services/registration/confirm_create_pin.vis index 1235916..02279dc 100644 --- a/services/registration/confirm_create_pin.vis +++ b/services/registration/confirm_create_pin.vis @@ -1,4 +1,4 @@ -LOAD save_pin 0 +LOAD save_temporary_pin 6 HALT -LOAD verify_pin 8 +LOAD verify_create_pin 8 INCMP account_creation * diff --git a/services/registration/confirm_others_new_pin b/services/registration/confirm_others_new_pin new file mode 100644 index 0000000..d345a0a --- /dev/null +++ b/services/registration/confirm_others_new_pin @@ -0,0 +1 @@ +Please confirm new PIN for:{{.retrieve_blocked_number}} \ No newline at end of file diff --git a/services/registration/confirm_others_new_pin.vis b/services/registration/confirm_others_new_pin.vis new file mode 100644 index 0000000..9132dc4 --- /dev/null +++ b/services/registration/confirm_others_new_pin.vis @@ -0,0 +1,14 @@ +CATCH pin_entry flag_incorrect_pin 1 +RELOAD retrieve_blocked_number +MAP retrieve_blocked_number +CATCH invalid_others_pin flag_valid_pin 0 +CATCH pin_reset_result flag_account_authorized 1 +LOAD save_others_temporary_pin 6 +RELOAD save_others_temporary_pin +MOUT back 0 +HALT +INCMP _ 0 +LOAD check_pin_mismatch 0 +RELOAD check_pin_mismatch +CATCH others_pin_mismatch flag_pin_mismatch 1 +INCMP pin_entry * diff --git a/services/registration/confirm_pin_change.vis b/services/registration/confirm_pin_change.vis index 7691e01..cf485a1 100644 --- a/services/registration/confirm_pin_change.vis +++ b/services/registration/confirm_pin_change.vis @@ -3,5 +3,3 @@ MOUT back 0 HALT INCMP _ 0 INCMP * pin_reset_success - - diff --git a/services/registration/create_pin.vis b/services/registration/create_pin.vis index e0e330f..40989ec 100644 --- a/services/registration/create_pin.vis +++ b/services/registration/create_pin.vis @@ -2,8 +2,8 @@ LOAD create_account 0 CATCH account_creation_failed flag_account_creation_failed 1 MOUT exit 0 HALT -LOAD save_pin 0 -RELOAD save_pin +LOAD save_temporary_pin 6 +RELOAD save_temporary_pin CATCH . flag_incorrect_pin 1 INCMP quit 0 INCMP confirm_create_pin * diff --git a/services/registration/edit_profile.vis b/services/registration/edit_profile.vis index 9d45ec9..277f330 100644 --- a/services/registration/edit_profile.vis +++ b/services/registration/edit_profile.vis @@ -1,4 +1,5 @@ LOAD reset_account_authorized 16 +RELOAD reset_account_authorized LOAD reset_allow_update 0 RELOAD reset_allow_update MOUT edit_name 1 diff --git a/services/registration/enter_familyname.vis b/services/registration/enter_familyname.vis index b9fe7b0..5db4c17 100644 --- a/services/registration/enter_familyname.vis +++ b/services/registration/enter_familyname.vis @@ -1,9 +1,8 @@ CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 -LOAD save_familyname 0 -RELOAD save_familyname +CATCH update_familyname flag_allow_update 1 MOUT back 0 HALT +LOAD save_familyname 0 RELOAD save_familyname INCMP _ 0 INCMP pin_entry * diff --git a/services/registration/enter_location.vis b/services/registration/enter_location.vis index fdd29ce..8966872 100644 --- a/services/registration/enter_location.vis +++ b/services/registration/enter_location.vis @@ -1,8 +1,8 @@ CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 -LOAD save_location 0 +CATCH update_location flag_allow_update 1 MOUT back 0 HALT +LOAD save_location 0 RELOAD save_location INCMP _ 0 INCMP pin_entry * diff --git a/services/registration/enter_name.vis b/services/registration/enter_name.vis index 563577e..f853d0a 100644 --- a/services/registration/enter_name.vis +++ b/services/registration/enter_name.vis @@ -1,12 +1,8 @@ CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 -LOAD save_firstname 0 -RELOAD save_firstname +CATCH update_firstname flag_allow_update 1 MOUT back 0 HALT +LOAD save_firstname 0 RELOAD save_firstname INCMP _ 0 INCMP pin_entry * - - - diff --git a/services/registration/enter_offerings.vis b/services/registration/enter_offerings.vis index 26e4b61..5cc7977 100644 --- a/services/registration/enter_offerings.vis +++ b/services/registration/enter_offerings.vis @@ -1,5 +1,5 @@ CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 +CATCH update_offerings flag_allow_update 1 LOAD save_offerings 0 MOUT back 0 HALT diff --git a/services/registration/enter_other_number b/services/registration/enter_other_number new file mode 100644 index 0000000..1c4a481 --- /dev/null +++ b/services/registration/enter_other_number @@ -0,0 +1 @@ +Enter other's phone number: \ No newline at end of file diff --git a/services/registration/enter_other_number.vis b/services/registration/enter_other_number.vis new file mode 100644 index 0000000..0957165 --- /dev/null +++ b/services/registration/enter_other_number.vis @@ -0,0 +1,7 @@ +CATCH no_admin_privilege flag_admin_privilege 0 +LOAD reset_account_authorized 0 +RELOAD reset_account_authorized +MOUT back 0 +HALT +INCMP _ 0 +INCMP enter_others_new_pin * diff --git a/services/registration/enter_others_new_pin b/services/registration/enter_others_new_pin new file mode 100644 index 0000000..52ae664 --- /dev/null +++ b/services/registration/enter_others_new_pin @@ -0,0 +1 @@ +Please enter new PIN for: {{.retrieve_blocked_number}} \ No newline at end of file diff --git a/services/registration/enter_others_new_pin.vis b/services/registration/enter_others_new_pin.vis new file mode 100644 index 0000000..7711c97 --- /dev/null +++ b/services/registration/enter_others_new_pin.vis @@ -0,0 +1,12 @@ +LOAD validate_blocked_number 6 +RELOAD validate_blocked_number +CATCH unregistered_number flag_unregistered_number 1 +LOAD retrieve_blocked_number 0 +RELOAD retrieve_blocked_number +MAP retrieve_blocked_number +MOUT back 0 +HALT +LOAD verify_new_pin 6 +RELOAD verify_new_pin +INCMP _ 0 +INCMP * confirm_others_new_pin diff --git a/services/registration/enter_yob.vis b/services/registration/enter_yob.vis index 40bf3f4..c74aeed 100644 --- a/services/registration/enter_yob.vis +++ b/services/registration/enter_yob.vis @@ -1,10 +1,10 @@ CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 -LOAD save_yob 0 +CATCH update_yob flag_allow_update 1 MOUT back 0 HALT LOAD verify_yob 0 CATCH incorrect_date_format flag_incorrect_date_format 1 +LOAD save_yob 0 RELOAD save_yob INCMP _ 0 INCMP pin_entry * diff --git a/services/registration/guard_pin_menu b/services/registration/guard_pin_menu deleted file mode 100644 index 9de0ba0..0000000 --- a/services/registration/guard_pin_menu +++ /dev/null @@ -1 +0,0 @@ -Guard my PIN \ No newline at end of file diff --git a/services/registration/guard_pin_menu_swa b/services/registration/guard_pin_menu_swa deleted file mode 100644 index c6b126f..0000000 --- a/services/registration/guard_pin_menu_swa +++ /dev/null @@ -1 +0,0 @@ -Linda PIN yangu \ No newline at end of file diff --git a/services/registration/invalid_others_pin b/services/registration/invalid_others_pin new file mode 100644 index 0000000..acdf45f --- /dev/null +++ b/services/registration/invalid_others_pin @@ -0,0 +1 @@ +The PIN you have entered is invalid.Please try a 4 digit number instead. \ No newline at end of file diff --git a/services/registration/invalid_others_pin.vis b/services/registration/invalid_others_pin.vis new file mode 100644 index 0000000..d218e6d --- /dev/null +++ b/services/registration/invalid_others_pin.vis @@ -0,0 +1,5 @@ +MOUT retry 1 +MOUT quit 9 +HALT +INCMP enter_others_new_pin 1 +INCMP quit 9 diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po index 5289dd7..ba9a9bb 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -1,12 +1,14 @@ msgid "Your account balance is %s" msgstr "Salio lako ni %s" -msgid "Your request has been sent. %s will receive %s from %s." -msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s." +msgid "Your request has been sent. %s will receive %s %s from %s." +msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s." msgid "Thank you for using Sarafu. Goodbye!" msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" - msgid "For more help,please call: 0757628885" msgstr "Kwa usaidizi zaidi,piga: 0757628885" + +msgid "Balance: %s\n" +msgstr "Salio: %s\n" diff --git a/services/registration/main b/services/registration/main index bf15ea5..afae8c1 100644 --- a/services/registration/main +++ b/services/registration/main @@ -1 +1 @@ -Balance: {{.check_balance}} +{{.check_balance}} \ No newline at end of file diff --git a/services/registration/main.vis b/services/registration/main.vis index d883dca..7e1c9bf 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -1,5 +1,10 @@ +LOAD set_default_voucher 8 +RELOAD set_default_voucher LOAD check_balance 64 RELOAD check_balance +LOAD check_vouchers 10 +RELOAD check_vouchers +CATCH api_failure flag_api_call_error 1 MAP check_balance MOUT send 1 MOUT vouchers 2 @@ -8,7 +13,7 @@ MOUT help 4 MOUT quit 9 HALT INCMP send 1 -INCMP quit 2 +INCMP my_vouchers 2 INCMP my_account 3 INCMP help 4 INCMP quit 9 diff --git a/services/registration/main_swa b/services/registration/main_swa index b72abf0..afae8c1 100644 --- a/services/registration/main_swa +++ b/services/registration/main_swa @@ -1 +1 @@ -Salio: {{.check_balance}} +{{.check_balance}} \ No newline at end of file diff --git a/services/registration/my_balance b/services/registration/my_balance index 15c5901..f8f8318 100644 --- a/services/registration/my_balance +++ b/services/registration/my_balance @@ -1 +1 @@ -Your balance is: 0.00 SRF \ No newline at end of file +{{.fetch_custodial_balances}} \ No newline at end of file diff --git a/services/registration/my_balance.vis b/services/registration/my_balance.vis index 151c6d8..85ae93a 100644 --- a/services/registration/my_balance.vis +++ b/services/registration/my_balance.vis @@ -1,5 +1,11 @@ -LOAD reset_incorrect 0 +LOAD reset_incorrect 6 +LOAD fetch_custodial_balances 0 +CATCH api_failure flag_api_call_error 1 +MAP fetch_custodial_balances CATCH incorrect_pin flag_incorrect_pin 1 CATCH pin_entry flag_account_authorized 0 -LOAD quit_with_balance 0 +MOUT back 0 +MOUT quit 9 HALT +INCMP _ 0 +INCMP quit 9 diff --git a/services/registration/my_vouchers b/services/registration/my_vouchers new file mode 100644 index 0000000..548de9c --- /dev/null +++ b/services/registration/my_vouchers @@ -0,0 +1 @@ +My vouchers \ No newline at end of file diff --git a/services/registration/my_vouchers.vis b/services/registration/my_vouchers.vis new file mode 100644 index 0000000..b59441a --- /dev/null +++ b/services/registration/my_vouchers.vis @@ -0,0 +1,8 @@ +LOAD reset_account_authorized 16 +RELOAD reset_account_authorized +MOUT select_voucher 1 +MOUT voucher_details 2 +MOUT back 0 +HALT +INCMP _ 0 +INCMP select_voucher 1 diff --git a/services/registration/new_pin b/services/registration/new_pin index bae2814..65d8ed3 100644 --- a/services/registration/new_pin +++ b/services/registration/new_pin @@ -1 +1 @@ -Enter a new four number pin +Enter a new four number PIN: diff --git a/services/registration/no_admin_privilege b/services/registration/no_admin_privilege new file mode 100644 index 0000000..27901dc --- /dev/null +++ b/services/registration/no_admin_privilege @@ -0,0 +1 @@ +You do not have privileges to perform this action diff --git a/services/registration/no_admin_privilege.vis b/services/registration/no_admin_privilege.vis new file mode 100644 index 0000000..3cf1e4c --- /dev/null +++ b/services/registration/no_admin_privilege.vis @@ -0,0 +1,5 @@ +MOUT quit 9 +MOUT back 0 +HALT +INCMP pin_management 0 +INCMP quit 9 diff --git a/services/registration/no_voucher b/services/registration/no_voucher new file mode 100644 index 0000000..332f00e --- /dev/null +++ b/services/registration/no_voucher @@ -0,0 +1 @@ +You need a voucher to send \ No newline at end of file diff --git a/services/registration/no_voucher.vis b/services/registration/no_voucher.vis new file mode 100644 index 0000000..832ef22 --- /dev/null +++ b/services/registration/no_voucher.vis @@ -0,0 +1,5 @@ +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/no_voucher_swa b/services/registration/no_voucher_swa new file mode 100644 index 0000000..66e8f26 --- /dev/null +++ b/services/registration/no_voucher_swa @@ -0,0 +1 @@ +Unahitaji sarafu kutuma \ No newline at end of file diff --git a/services/registration/others_pin_mismatch b/services/registration/others_pin_mismatch new file mode 100644 index 0000000..deb9fe5 --- /dev/null +++ b/services/registration/others_pin_mismatch @@ -0,0 +1 @@ +The PIN you have entered is not a match diff --git a/services/registration/others_pin_mismatch.vis b/services/registration/others_pin_mismatch.vis new file mode 100644 index 0000000..37b3deb --- /dev/null +++ b/services/registration/others_pin_mismatch.vis @@ -0,0 +1,5 @@ +MOUT retry 1 +MOUT quit 9 +HALT +INCMP _ 1 +INCMP quit 9 diff --git a/services/registration/pin_management.vis b/services/registration/pin_management.vis index 3b33dad..5eb7d5a 100644 --- a/services/registration/pin_management.vis +++ b/services/registration/pin_management.vis @@ -1,8 +1,8 @@ MOUT change_pin 1 MOUT reset_pin 2 -MOUT guard_pin 3 MOUT back 0 HALT -INCMP _ 0 +INCMP my_account 0 INCMP old_pin 1 - +INCMP enter_other_number 2 +INCMP . * diff --git a/services/registration/pin_reset_result b/services/registration/pin_reset_result new file mode 100644 index 0000000..60554b9 --- /dev/null +++ b/services/registration/pin_reset_result @@ -0,0 +1 @@ +PIN reset request for {{.retrieve_blocked_number}} was successful \ No newline at end of file diff --git a/services/registration/pin_reset_result.vis b/services/registration/pin_reset_result.vis new file mode 100644 index 0000000..34b9789 --- /dev/null +++ b/services/registration/pin_reset_result.vis @@ -0,0 +1,8 @@ +LOAD retrieve_blocked_number 0 +MAP retrieve_blocked_number +LOAD reset_others_pin 6 +MOUT back 0 +MOUT quit 9 +HALT +INCMP pin_management 0 +INCMP quit 9 diff --git a/services/registration/pin_reset_success.vis b/services/registration/pin_reset_success.vis index c942519..96dee73 100644 --- a/services/registration/pin_reset_success.vis +++ b/services/registration/pin_reset_success.vis @@ -6,5 +6,3 @@ MOUT quit 9 HALT INCMP main 0 INCMP quit 9 - - diff --git a/services/registration/pp.csv b/services/registration/pp.csv index fd552e4..406cc22 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -14,3 +14,8 @@ flag,flag_valid_pin,20,this is set when the given PIN is valid flag,flag_allow_update,21,this is set to allow a user to update their profile data flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid +flag,flag_incorrect_voucher,24,this is set when the selected voucher is invalid +flag,flag_api_call_error,25,this is set when communication to an external service fails +flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher +flag,flag_admin_privilege,27,this is set when a user has admin privileges. +flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action diff --git a/services/registration/root.vis b/services/registration/root.vis index 6e3b79d..02ef9e9 100644 --- a/services/registration/root.vis +++ b/services/registration/root.vis @@ -1,6 +1,8 @@ CATCH select_language flag_language_set 0 CATCH terms flag_account_created 0 LOAD check_account_status 0 +RELOAD check_account_status +CATCH api_failure flag_api_call_error 1 CATCH account_pending flag_account_pending 1 CATCH create_pin flag_pin_set 0 CATCH main flag_account_success 1 diff --git a/services/registration/select_voucher b/services/registration/select_voucher new file mode 100644 index 0000000..084b9b8 --- /dev/null +++ b/services/registration/select_voucher @@ -0,0 +1,2 @@ +Select number or symbol from your vouchers: +{{.get_vouchers}} \ No newline at end of file diff --git a/services/registration/select_voucher.vis b/services/registration/select_voucher.vis new file mode 100644 index 0000000..08aa434 --- /dev/null +++ b/services/registration/select_voucher.vis @@ -0,0 +1,15 @@ +LOAD get_vouchers 0 +MAP get_vouchers +MOUT back 0 +MOUT quit 99 +MNEXT next 11 +MPREV prev 22 +HALT +LOAD view_voucher 80 +RELOAD view_voucher +CATCH . flag_incorrect_voucher 1 +INCMP _ 0 +INCMP quit 99 +INCMP > 11 +INCMP < 22 +INCMP view_voucher * diff --git a/services/registration/select_voucher_menu b/services/registration/select_voucher_menu new file mode 100644 index 0000000..8ee06df --- /dev/null +++ b/services/registration/select_voucher_menu @@ -0,0 +1 @@ +Select voucher \ No newline at end of file diff --git a/services/registration/select_voucher_swa b/services/registration/select_voucher_swa new file mode 100644 index 0000000..b4720bf --- /dev/null +++ b/services/registration/select_voucher_swa @@ -0,0 +1,2 @@ +Chagua nambari au ishara kutoka kwa salio zako: +{{.get_vouchers}} \ No newline at end of file diff --git a/services/registration/send.vis b/services/registration/send.vis index e120302..0ff0927 100644 --- a/services/registration/send.vis +++ b/services/registration/send.vis @@ -1,4 +1,5 @@ LOAD transaction_reset 0 +CATCH no_voucher flag_no_active_voucher 1 MOUT back 0 HALT LOAD validate_recipient 20 diff --git a/services/registration/set_female.vis b/services/registration/set_female.vis index 723b080..e211ada 100644 --- a/services/registration/set_female.vis +++ b/services/registration/set_female.vis @@ -1,4 +1,4 @@ LOAD save_gender 0 CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 +CATCH update_gender flag_allow_update 1 MOVE pin_entry diff --git a/services/registration/set_male.vis b/services/registration/set_male.vis index 723b080..e211ada 100644 --- a/services/registration/set_male.vis +++ b/services/registration/set_male.vis @@ -1,4 +1,4 @@ LOAD save_gender 0 CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 +CATCH update_gender flag_allow_update 1 MOVE pin_entry diff --git a/services/registration/set_unspecified.vis b/services/registration/set_unspecified.vis index 723b080..e211ada 100644 --- a/services/registration/set_unspecified.vis +++ b/services/registration/set_unspecified.vis @@ -1,4 +1,4 @@ LOAD save_gender 0 CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 +CATCH update_gender flag_allow_update 1 MOVE pin_entry diff --git a/services/registration/unregistered_number b/services/registration/unregistered_number new file mode 100644 index 0000000..9cc33d7 --- /dev/null +++ b/services/registration/unregistered_number @@ -0,0 +1 @@ +The number you have entered is either not registered with Sarafu or is invalid. \ No newline at end of file diff --git a/services/registration/unregistered_number.vis b/services/registration/unregistered_number.vis new file mode 100644 index 0000000..0ff96be --- /dev/null +++ b/services/registration/unregistered_number.vis @@ -0,0 +1,7 @@ +LOAD reset_unregistered_number 0 +RELOAD reset_unregistered_number +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/update_age b/services/registration/update_age new file mode 100644 index 0000000..76ca306 --- /dev/null +++ b/services/registration/update_age @@ -0,0 +1,2 @@ +RELOAD save_yob +CATCH profile_update_success flag_allow_update 1 \ No newline at end of file diff --git a/services/registration/update_familyname.vis b/services/registration/update_familyname.vis new file mode 100644 index 0000000..7cd4d9f --- /dev/null +++ b/services/registration/update_familyname.vis @@ -0,0 +1,2 @@ +RELOAD save_familyname +CATCH profile_update_success flag_allow_update 1 diff --git a/services/registration/update_firstname.vis b/services/registration/update_firstname.vis new file mode 100644 index 0000000..dca7036 --- /dev/null +++ b/services/registration/update_firstname.vis @@ -0,0 +1,2 @@ +RELOAD save_firstname +CATCH profile_update_success flag_allow_update 1 diff --git a/services/registration/update_gender.vis b/services/registration/update_gender.vis new file mode 100644 index 0000000..506a56a --- /dev/null +++ b/services/registration/update_gender.vis @@ -0,0 +1,2 @@ +RELOAD save_gender +CATCH profile_update_success flag_allow_update 1 diff --git a/services/registration/update_location.vis b/services/registration/update_location.vis new file mode 100644 index 0000000..16c4ea2 --- /dev/null +++ b/services/registration/update_location.vis @@ -0,0 +1,2 @@ +RELOAD save_location +CATCH profile_update_success flag_allow_update 1 diff --git a/services/registration/update_offerings.vis b/services/registration/update_offerings.vis new file mode 100644 index 0000000..4aeed74 --- /dev/null +++ b/services/registration/update_offerings.vis @@ -0,0 +1,2 @@ +RELOAD save_offerings +CATCH profile_update_success flag_allow_update 1 diff --git a/services/registration/update_yob.vis b/services/registration/update_yob.vis new file mode 100644 index 0000000..a9388ae --- /dev/null +++ b/services/registration/update_yob.vis @@ -0,0 +1,2 @@ +RELOAD save_yob +CATCH profile_update_success flag_allow_update 1 diff --git a/services/registration/view_menu_swa b/services/registration/view_menu_swa new file mode 100644 index 0000000..bd84b19 --- /dev/null +++ b/services/registration/view_menu_swa @@ -0,0 +1 @@ +Angalia Wasifu \ No newline at end of file diff --git a/services/registration/view_voucher b/services/registration/view_voucher new file mode 100644 index 0000000..3940982 --- /dev/null +++ b/services/registration/view_voucher @@ -0,0 +1,2 @@ +Enter PIN to confirm selection: +{{.view_voucher}} \ No newline at end of file diff --git a/services/registration/view_voucher.vis b/services/registration/view_voucher.vis new file mode 100644 index 0000000..1480099 --- /dev/null +++ b/services/registration/view_voucher.vis @@ -0,0 +1,10 @@ +MAP view_voucher +MOUT back 0 +MOUT quit 9 +LOAD authorize_account 6 +HALT +RELOAD authorize_account +CATCH incorrect_pin flag_incorrect_pin 1 +INCMP _ 0 +INCMP quit 9 +INCMP voucher_set * diff --git a/services/registration/view_voucher_swa b/services/registration/view_voucher_swa new file mode 100644 index 0000000..485e2ef --- /dev/null +++ b/services/registration/view_voucher_swa @@ -0,0 +1,2 @@ +Weka PIN ili kuthibitisha chaguo: +{{.view_voucher}} \ No newline at end of file diff --git a/services/registration/voucher_details_menu b/services/registration/voucher_details_menu new file mode 100644 index 0000000..a588f23 --- /dev/null +++ b/services/registration/voucher_details_menu @@ -0,0 +1 @@ +Voucher details \ No newline at end of file diff --git a/services/registration/voucher_set b/services/registration/voucher_set new file mode 100644 index 0000000..e90d2e5 --- /dev/null +++ b/services/registration/voucher_set @@ -0,0 +1 @@ +Success! {{.set_voucher}} is now your active voucher. \ No newline at end of file diff --git a/services/registration/voucher_set.vis b/services/registration/voucher_set.vis new file mode 100644 index 0000000..e75c693 --- /dev/null +++ b/services/registration/voucher_set.vis @@ -0,0 +1,10 @@ +LOAD reset_incorrect 6 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH _ flag_account_authorized 0 +LOAD set_voucher 12 +MAP set_voucher +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/voucher_set_swa b/services/registration/voucher_set_swa new file mode 100644 index 0000000..97d3fde --- /dev/null +++ b/services/registration/voucher_set_swa @@ -0,0 +1 @@ +Hongera! {{.set_voucher}} ni Sarafu inayotumika sasa. \ No newline at end of file