diff --git a/.env.example b/.env.example index 97d3317..c636fa8 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,9 @@ PORT=7123 HOST=127.0.0.1 +#AfricasTalking USSD POST endpoint +AT_ENDPOINT=/ussd/africastalking + #PostgreSQL DB_HOST=localhost DB_USER=postgres @@ -12,6 +15,6 @@ DB_SSLMODE=disable DB_TIMEZONE=Africa/Nairobi #External API Calls -CREATE_ACCOUNT_URL=https://custodial.sarafu.africa/api/account/create -TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/ -BALANCE_URL=https://custodial.sarafu.africa/api/account/status/ +CUSTODIAL_URL_BASE=http://localhost:5003 +BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr +DATA_URL_BASE=http://localhost:5006 diff --git a/.gitignore b/.gitignore index ddccccf..b523c77 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ go.work* cmd/.state/ id_* *.gdbm +*.log diff --git a/README.md b/README.md index 35ef7f1..493dd96 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,91 @@ -# ussd +# URDT USSD service -> USSD +This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine. -USSD service. +## Prerequisites +### 1. [go-vise](https://github.com/nolash/go-vise) + +Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git` +``` +git clone https://git.defalsify.org/vise.git +``` + +## Setup +1. Clone the ussd repo in its own directory + + ``` + git clone https://git.grassecon.net/urdt/ussd.git + ``` + +2. Navigate to the project directory. +3. Enter the `services/registration` subfolder: + ``` + cd services/registration + ``` +4. make the .bin files from the .vis files + ``` + make VISE_PATH=/var/path/to/your/go-vise -B + ``` +5. Return to the project root (`cd ../..`) +6. Run the USSD menu + ``` + go run cmd/main.go -session-id=0712345678 + ``` +## Running the different binaries +1. ### CLI: + ``` + go run cmd/main.go -session-id=0712345678 + ``` +2. ### Africastalking: + ``` + go run cmd/africastalking/main.go + ``` +3. ### Async: + ``` + go run cmd/async/main.go + ``` +4. ### Http: + ``` + go run cmd/http/main.go + ``` + +## Flags +Below are the supported flags: + +1. `-session-id`: + + Specifies the session ID. (CLI only). + + Default: `075xx2123`. + + Example: + ``` + go run cmd/main.go -session-id=0712345678 + ``` + +2. `-d`: + + Enables engine debug output. + + Default: `false`. + + Example: + ``` + go run cmd/main.go -session-id=0712345678 -d + ``` + +3. `-db`: + + Specifies the database type. + + Default: `gdbm`. + + Example: + ``` + go run cmd/main.go -session-id=0712345678 -d -db=postgres + ``` + + >Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file. ## License diff --git a/cmd/africastalking/main.go b/cmd/africastalking/main.go index 79fb091..5a523e7 100644 --- a/cmd/africastalking/main.go +++ b/cmd/africastalking/main.go @@ -1,9 +1,13 @@ package main import ( + "bytes" "context" + "encoding/json" "flag" "fmt" + "io" + "log" "net/http" "os" "os/signal" @@ -19,14 +23,14 @@ import ( "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/handlers/server" httpserver "git.grassecon.net/urdt/ussd/internal/http" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( - logg = logging.NewVanilla() - scriptDir = path.Join("services", "registration") + logg = logging.NewVanilla() + scriptDir = path.Join("services", "registration") ) func init() { @@ -38,9 +42,30 @@ type atRequestParser struct{} func (arp *atRequestParser) GetSessionId(rq any) (string, error) { rqv, ok := rq.(*http.Request) if !ok { + log.Println("got an invalid request:", rq) return "", handlers.ErrInvalidRequest } + + // Capture body (if any) for logging + body, err := io.ReadAll(rqv.Body) + if err != nil { + log.Println("failed to read request body:", err) + return "", fmt.Errorf("failed to read request body: %v", err) + } + // Reset the body for further reading + rqv.Body = io.NopCloser(bytes.NewReader(body)) + + // Log the body as JSON + bodyLog := map[string]string{"body": string(body)} + logBytes, err := json.Marshal(bodyLog) + if err != nil { + log.Println("failed to marshal request body:", err) + } else { + log.Println("Received request:", string(logBytes)) + } + if err := rqv.ParseForm(); err != nil { + log.Println("failed to parse form data: %v", err) return "", fmt.Errorf("failed to parse form data: %v", err) } @@ -131,7 +156,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 { @@ -139,7 +164,7 @@ func main() { os.Exit(1) } - accountService := server.AccountService{} + accountService := remote.AccountService{} hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) @@ -156,9 +181,13 @@ func main() { rp := &atRequestParser{} bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) sh := httpserver.NewATSessionHandler(bsh) + + mux := http.NewServeMux() + mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) + s := &http.Server{ Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), - Handler: sh, + Handler: mux, } s.RegisterOnShutdown(sh.Shutdown) diff --git a/cmd/async/main.go b/cmd/async/main.go index 902dfc7..e4c94b0 100644 --- a/cmd/async/main.go +++ b/cmd/async/main.go @@ -16,8 +16,8 @@ import ( "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/handlers/server" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -104,9 +104,9 @@ 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 := server.AccountService{} + accountService := remote.AccountService{} hl, err := lhs.GetHandler(&accountService) if err != nil { diff --git a/cmd/http/main.go b/cmd/http/main.go index ece882e..96e2688 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -18,9 +18,9 @@ import ( "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/handlers/server" httpserver "git.grassecon.net/urdt/ussd/internal/http" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -92,14 +92,15 @@ 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 { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } - accountService := server.AccountService{} + + accountService := remote.AccountService{} hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) diff --git a/cmd/main.go b/cmd/main.go index 21ca0c3..9599eb7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,8 +13,8 @@ import ( "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/handlers/server" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/remote" ) var ( @@ -88,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) @@ -97,7 +97,7 @@ func main() { os.Exit(1) } - accountService := server.AccountService{} + accountService := remote.AccountService{} hl, err := lhs.GetHandler(&accountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) diff --git a/common/db.go b/common/db.go new file mode 100644 index 0000000..e90fd47 --- /dev/null +++ b/common/db.go @@ -0,0 +1,71 @@ +package common + +import ( + "encoding/binary" + "errors" + + "git.defalsify.org/vise.git/logging" +) + +type DataTyp uint16 + +const ( + DATA_ACCOUNT DataTyp = iota + DATA_ACCOUNT_CREATED + DATA_TRACKING_ID + DATA_PUBLIC_KEY + DATA_CUSTODIAL_ID + DATA_ACCOUNT_PIN + DATA_ACCOUNT_STATUS + DATA_FIRST_NAME + DATA_FAMILY_NAME + DATA_YOB + DATA_LOCATION + DATA_GENDER + DATA_OFFERINGS + DATA_RECIPIENT + DATA_AMOUNT + 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 { + var b [2]byte + binary.BigEndian.PutUint16(b[:], uint16(typ)) + return b[:] +} + +func PackKey(typ DataTyp, data []byte) []byte { + v := typToBytes(typ) + return append(v, data...) +} + +func StringToDataTyp(str string) (DataTyp, error) { + switch str { + case "DATA_FIRST_NAME": + return DATA_FIRST_NAME, nil + case "DATA_FAMILY_NAME": + return DATA_FAMILY_NAME, nil + case "DATA_YOB": + return DATA_YOB, nil + case "DATA_LOCATION": + return DATA_LOCATION, nil + case "DATA_GENDER": + return DATA_GENDER, nil + case "DATA_OFFERINGS": + return DATA_OFFERINGS, nil + + default: + return 0, errors.New("invalid DataTyp string") + } +} 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/common/tokens.go b/common/tokens.go new file mode 100644 index 0000000..466f370 --- /dev/null +++ b/common/tokens.go @@ -0,0 +1,81 @@ +package common + +import ( + "context" + "errors" + "math/big" + "reflect" + "strconv" +) + +type TransactionData struct { + TemporaryValue string + ActiveSym string + Amount string + PublicKey string + Recipient string + ActiveDecimal string + ActiveAddress string +} + +func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) { + // Parse token decimal + tokenDecimal, err := strconv.Atoi(activeDecimal) + if err != nil { + + return "", err + } + + // Parse amount + amount, _, err := big.ParseFloat(storedAmount, 10, 0, big.ToZero) + if err != nil { + return "", err + } + + // Scale the amount + multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimal)), nil)) + finalAmount := new(big.Float).Mul(amount, multiplier) + + // Convert finalAmount to a string + finalAmountStr := new(big.Int) + finalAmount.Int(finalAmountStr) + + return finalAmountStr.String(), nil +} + +func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) { + data := TransactionData{} + fieldToKey := map[string]DataTyp{ + "TemporaryValue": DATA_TEMPORARY_VALUE, + "ActiveSym": DATA_ACTIVE_SYM, + "Amount": DATA_AMOUNT, + "PublicKey": DATA_PUBLIC_KEY, + "Recipient": DATA_RECIPIENT, + "ActiveDecimal": DATA_ACTIVE_DECIMAL, + "ActiveAddress": DATA_ACTIVE_ADDRESS, + } + + v := reflect.ValueOf(&data).Elem() + for fieldName, key := range fieldToKey { + field := v.FieldByName(fieldName) + if !field.IsValid() || !field.CanSet() { + return data, errors.New("invalid struct field: " + fieldName) + } + + value, err := readStringEntry(ctx, store, sessionId, key) + if err != nil { + return data, err + } + field.SetString(value) + } + + return data, nil +} + +func readStringEntry(ctx context.Context, store DataStore, sessionId string, key DataTyp) (string, error) { + entry, err := store.ReadEntry(ctx, sessionId, key) + if err != nil { + return "", err + } + return string(entry), nil +} diff --git a/common/tokens_test.go b/common/tokens_test.go new file mode 100644 index 0000000..06bd552 --- /dev/null +++ b/common/tokens_test.go @@ -0,0 +1,129 @@ +package common + +import ( + "testing" + + "github.com/alecthomas/assert/v2" +) + +func TestParseAndScaleAmount(t *testing.T) { + tests := []struct { + name string + amount string + decimals string + want string + expectError bool + }{ + { + name: "whole number", + amount: "123", + decimals: "2", + want: "12300", + expectError: false, + }, + { + name: "decimal number", + amount: "123.45", + decimals: "2", + want: "12345", + expectError: false, + }, + { + name: "zero decimals", + amount: "123.45", + decimals: "0", + want: "123", + expectError: false, + }, + { + name: "large number", + amount: "1000000.01", + decimals: "6", + want: "1000000010000", + expectError: false, + }, + { + name: "invalid amount", + amount: "abc", + decimals: "2", + want: "", + expectError: true, + }, + { + name: "invalid decimals", + amount: "123.45", + decimals: "abc", + want: "", + expectError: true, + }, + { + name: "zero amount", + amount: "0", + decimals: "2", + want: "0", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseAndScaleAmount(tt.amount, tt.decimals) + + // Check error cases + if tt.expectError { + if err == nil { + t.Errorf("ParseAndScaleAmount(%q, %q) expected error, got nil", tt.amount, tt.decimals) + } + return + } + + if err != nil { + t.Errorf("ParseAndScaleAmount(%q, %q) unexpected error: %v", tt.amount, tt.decimals, err) + return + } + + if got != tt.want { + t.Errorf("ParseAndScaleAmount(%q, %q) = %v, want %v", tt.amount, tt.decimals, got, tt.want) + } + }) + } +} + +func TestReadTransactionData(t *testing.T) { + sessionId := "session123" + publicKey := "0X13242618721" + ctx, store := InitializeTestDb(t) + + // Test transaction data + transactionData := map[DataTyp]string{ + DATA_TEMPORARY_VALUE: "0712345678", + DATA_ACTIVE_SYM: "SRF", + DATA_AMOUNT: "1000000", + DATA_PUBLIC_KEY: publicKey, + DATA_RECIPIENT: "0x41c188d63Qa", + DATA_ACTIVE_DECIMAL: "6", + DATA_ACTIVE_ADDRESS: "0xd4c288865Ce", + } + + // Store the data + for key, value := range transactionData { + if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { + t.Fatal(err) + } + } + + expectedResult := TransactionData{ + TemporaryValue: "0712345678", + ActiveSym: "SRF", + Amount: "1000000", + PublicKey: publicKey, + Recipient: "0x41c188d63Qa", + ActiveDecimal: "6", + ActiveAddress: "0xd4c288865Ce", + } + + data, err := ReadTransactionData(ctx, store, sessionId) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, data) +} 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..81ca55f --- /dev/null +++ b/common/vouchers.go @@ -0,0 +1,173 @@ +package common + +import ( + "context" + "fmt" + "math/big" + "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)) + + // Scale down the balance + scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals) + + balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance)) + 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 ScaleDownBalance(balance, decimals string) string { + // Convert balance and decimals to big.Float + bal := new(big.Float) + bal.SetString(balance) + + dec, ok := new(big.Int).SetString(decimals, 10) + if !ok { + dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure + } + + divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil)) + scaledBalance := new(big.Float).Quo(bal, divisor) + + // Return the scaled balance without trailing decimals if it's an integer + if scaledBalance.IsInt() { + return scaledBalance.Text('f', 0) + } + return scaledBalance.Text('f', -1) +} + +// 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 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..9f5bf85 --- /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: "100000000"}, + {ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"}, + } + + expectedResult := VoucherMetadata{ + Symbols: "1:SRF\n2:MILO", + Balances: "1:100\n2:20000", + 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 dba0da7..edb33fe 100644 --- a/config/config.go +++ b/config/config.go @@ -1,16 +1,71 @@ package config -import "git.grassecon.net/urdt/ussd/initializers" +import ( + "net/url" -var ( - CreateAccountURL string - TrackStatusURL string - BalanceURL string + "git.grassecon.net/urdt/ussd/initializers" ) -// LoadConfig initializes the configuration values after environment variables are loaded. -func LoadConfig() { - CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "https://custodial.sarafu.africa/api/account/create") - TrackStatusURL = initializers.GetEnv("TRACK_STATUS_URL", "https://custodial.sarafu.africa/api/track/") - BalanceURL = initializers.GetEnv("BALANCE_URL", "https://custodial.sarafu.africa/api/account/status/") +const ( + createAccountPath = "/api/v2/account/create" + trackStatusPath = "/api/track" + balancePathPrefix = "/api/account" + trackPath = "/api/v2/account/status" + tokenTransferPrefix = "/api/v2/token/transfer" + voucherHoldingsPathPrefix = "/api/v1/holdings" + voucherTransfersPathPrefix = "/api/v1/transfers/last10" + voucherDataPathPrefix = "/api/v1/token" +) + +var ( + custodialURLBase string + dataURLBase string + BearerToken string +) + +var ( + CreateAccountURL string + TrackStatusURL string + BalanceURL string + TrackURL string + TokenTransferURL 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") + BearerToken = initializers.GetEnv("BEARER_TOKEN", "") + + _, 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) + TokenTransferURL, _ = url.JoinPath(custodialURLBase, tokenTransferPrefix) + 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 38be305..391c1a5 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +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.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/joho/godotenv v1.5.1 // indirect +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 2abec41..0ba38c1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -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.0 h1:X2ZgiGRq4C+9qOlDMP0b/oE5QHjVQNT4aEFZB88ST0Q= -git.defalsify.org/vise.git v0.2.0/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= @@ -12,33 +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/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index c291268..6d21d88 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -1,13 +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/server" + "git.grassecon.net/urdt/ussd/internal/handlers/ussd" + "git.grassecon.net/urdt/ussd/internal/utils" + "git.grassecon.net/urdt/ussd/remote" ) type HandlerService interface { @@ -28,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 } @@ -53,16 +63,16 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) { ls.UserdataStore = db } -func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceInterface) (*ussd.Handlers, error) { - ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore,accountService) +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) @@ -70,6 +80,7 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) + ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient) ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) @@ -89,11 +100,24 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn 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("fetch_community_balance", ussdHandlers.FetchCommunityBalance) + 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("get_voucher_details", ussdHandlers.GetVoucherDetails) + 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) + ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactions) return ussdHandlers, nil diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go deleted file mode 100644 index 5b71e6f..0000000 --- a/internal/handlers/server/accountservice.go +++ /dev/null @@ -1,153 +0,0 @@ -package server - -import ( - "encoding/json" - "io" - "net/http" - "time" - - "git.grassecon.net/urdt/ussd/config" - "git.grassecon.net/urdt/ussd/internal/models" -) - -type AccountServiceInterface interface { - CheckBalance(publicKey string) (*models.BalanceResponse, error) - CreateAccount() (*models.AccountResponse, error) - CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) -} - -type AccountService struct { -} - -type TestAccountService 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) (*models.TrackStatusResponse, error) { - resp, err := http.Get(config.TrackStatusURL + trackingId) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var trackResp models.TrackStatusResponse - err = json.Unmarshal(body, &trackResp) - if err != nil { - return nil, err - } - return &trackResp, 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) (*models.BalanceResponse, error) { - resp, err := http.Get(config.BalanceURL + publicKey) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var balanceResp models.BalanceResponse - err = json.Unmarshal(body, &balanceResp) - if err != nil { - return nil, err - } - return &balanceResp, 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 -} - -func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) { - return &models.AccountResponse{ - Ok: true, - Result: struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - }{ - CustodialId: json.Number("182"), - PublicKey: "0x48ADca309b5085852207FAaf2816eD72B52F527C", - TrackingId: "28ebe84d-b925-472c-87ae-bbdfa1fb97be", - }, - }, nil -} - -func (tas *TestAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { - - balanceResponse := &models.BalanceResponse{ - Ok: true, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - } - - return balanceResponse, nil -} - -func (tas *TestAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { - trackResponse := &models.TrackStatusResponse{ - Ok: true, - 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\"" - } - }{ - Transaction: models.Transaction{ - CreatedAt: time.Now(), - Status: "SUCCESS", - TransferValue: json.Number("0.5"), - TxHash: "0x123abc456def", - TxType: "transfer", - }, - }, - } - return trackResponse, nil -} diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index ec27fea..8b1ceb4 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, accountService server.AccountServiceInterface) (*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, + 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") @@ -128,6 +161,7 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r languageSetFlag, err := h.flagManager.GetFlag("flag_language_set") if err != nil { + logg.ErrorCtxf(ctx, "Error setting the languageSetFlag", "error", err) return res, err } res.FlagSet = append(res.FlagSet, languageSetFlag) @@ -136,24 +170,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 } // CreateAccount checks if any account exists on the JSON data file, and if not @@ -167,44 +212,44 @@ 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") + logg.InfoCtxf(ctx, "Creating an account because it doesn't exist") err = h.createAccountNoExist(ctx, sessionId, &res) if err != nil { + logg.ErrorCtxf(ctx, "failed on createAccountNoExist", "error", err) return res, err } } } + 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 { + logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err) return res, err } + temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err) + 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 +271,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 +282,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 +291,39 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write temporaryAccountPIN entry with", "key", common.DATA_TEMPORARY_VALUE, "value", accountPIN, "error", err) 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 { + logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err) + return res, err + } + + err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryPin, "error", err) + return res, err + } + return res, nil } @@ -260,8 +336,9 @@ 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 { + logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err) return res, err } if bytes.Equal(temporaryPin, input) { @@ -269,17 +346,18 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err) 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") @@ -291,12 +369,12 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res return res, fmt.Errorf("missing session") } 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 { + logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err) 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) @@ -304,6 +382,12 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err) + return res, err + } + return res, nil } @@ -325,11 +409,21 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write firstName entry with", "key", common.DATA_FIRST_NAME, "value", temporaryFirstName, "error", err) + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(firstName)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryFirstName entry with", "key", common.DATA_TEMPORARY_VALUE, "value", firstName, "error", err) return res, err } } @@ -346,15 +440,25 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write familyName entry with", "key", common.DATA_FAMILY_NAME, "value", temporaryFamilyName, "error", err) 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 { + logg.ErrorCtxf(ctx, "failed to write temporaryFamilyName entry with", "key", common.DATA_TEMPORARY_VALUE, "value", familyName, "error", err) + return res, err + } } return res, nil @@ -368,12 +472,22 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write yob entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryYob, "error", err) + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(yob)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryYob entry with", "key", common.DATA_TEMPORARY_VALUE, "value", yob, "error", err) return res, err } } @@ -389,12 +503,23 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write location entry with", "key", common.DATA_LOCATION, "value", temporaryLocation, "error", err) + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(location)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryLocation entry with", "key", common.DATA_TEMPORARY_VALUE, "value", location, "error", err) return res, err } } @@ -411,12 +536,24 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write gender entry with", "key", common.DATA_GENDER, "value", gender, "error", err) + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(gender)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryGender entry with", "key", common.DATA_TEMPORARY_VALUE, "value", gender, "error", err) + return res, err + } } return res, nil @@ -431,12 +568,24 @@ 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 + logg.ErrorCtxf(ctx, "failed to write offerings entry with", "key", common.DATA_TEMPORARY_VALUE, "value", offerings, "error", err) + return res, err + } + } else { + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(offerings)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryOfferings entry with", "key", common.DATA_TEMPORARY_VALUE, "value", offerings, "error", err) + return res, err } } @@ -446,19 +595,23 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) // ResetAllowUpdate resets the allowupdate flag that allows a user to update profile data. func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") - res.FlagReset = append(res.FlagReset, flag_allow_update) 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) return res, nil } @@ -466,14 +619,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) @@ -485,19 +636,18 @@ 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 { + logg.ErrorCtxf(ctx, "failed to read AccountPin entry with", "key", common.DATA_ACCOUNT_PIN, "error", err) return res, err } if len(input) == 4 { @@ -541,35 +691,31 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b 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 { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) return res, err } - accountStatus, err := h.accountService.CheckAccountStatus(string(trackingId)) + r, err := h.accountService.TrackAccountStatus(ctx, string(publicKey)) if err != nil { - fmt.Println("Error checking account status:", err) - return res, err - } - if !accountStatus.Ok { res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on TrackAccountStatus", "error", err) return res, err } - res.FlagReset = append(res.FlagReset, flag_api_error) - status := accountStatus.Result.Transaction.Status - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)) - if err != nil { - return res, nil - } - if accountStatus.Result.Transaction.Status == "SUCCESS" { + res.FlagReset = append(res.FlagReset, flag_api_error) + + if r.Active { res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagReset = append(res.FlagReset, flag_account_pending) } else { res.FlagReset = append(res.FlagReset, flag_account_success) res.FlagSet = append(res.FlagSet, flag_account_pending) } + return res, nil } @@ -609,7 +755,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 { @@ -632,80 +777,123 @@ 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 var err error - flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") - sessionId, ok := ctx.Value("SessionId").(string) if !ok { 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 + } + + logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", common.DATA_ACTIVE_SYM, "error", err) return res, err } - balanceResponse, err := h.accountService.CheckBalance(string(publicKey)) + activeBal, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_BAL) if err != nil { - return res, nil + logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", common.DATA_ACTIVE_BAL, "error", err) + return res, err } - if !balanceResponse.Ok { - res.FlagSet = append(res.FlagSet, flag_api_error) - return res, nil - } - res.FlagReset = append(res.FlagReset, flag_api_error) - balance := balanceResponse.Result.Balance - res.Content = balance + + 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) { +func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + //TODO: + //Check if the address is a community account,if then,get the actual balance + res.Content = l.Get("Community Balance: 0.00") + 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") } - symbol, _ := h.st.Where() - balanceType := strings.Split(symbol, "_")[0] - - store := h.userdataStore - publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + blockedPhonenumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER) if err != nil { + logg.ErrorCtxf(ctx, "failed to read blockedPhonenumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err) return res, err } - - balanceResponse, err := h.accountService.CheckBalance(string(publicKey)) + temporaryPin, err := store.ReadEntry(ctx, string(blockedPhonenumber), common.DATA_TEMPORARY_VALUE) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err) + return res, err + } + err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(temporaryPin)) if err != nil { return res, nil } - if !balanceResponse.Ok { - res.FlagSet = append(res.FlagSet, flag_api_error) + + 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 } - res.FlagReset = append(res.FlagReset, flag_api_error) - balance := balanceResponse.Result.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 + if err != nil { + if db.IsNotFound(err) { + logg.InfoCtxf(ctx, "Invalid or unregistered number") + res.FlagSet = append(res.FlagSet, flag_unregistered_number) + return res, nil + } else { + logg.ErrorCtxf(ctx, "Error on ValidateBlockedNumber", "error", err) + return res, err + } + } + err = store.WriteEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER, []byte(blockedNumber)) + if err != nil { + return res, nil } return res, nil } @@ -714,6 +902,7 @@ func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input func (h *Handlers) ValidateRecipient(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 { @@ -723,18 +912,41 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by recipient := string(input) flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") + flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") if recipient != "0" { - // mimic invalid number check - if recipient == "000" { + if !isValidPhoneNumber(recipient) { res.FlagSet = append(res.FlagSet, flag_invalid_recipient) res.Content = recipient return res, nil } - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_RECIPIENT, []byte(recipient)) + + // save the recipient as the temporaryRecipient + err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(recipient)) if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", common.DATA_TEMPORARY_VALUE, "value", recipient, "error", err) + return res, err + } + + publicKey, err := store.ReadEntry(ctx, recipient, common.DATA_PUBLIC_KEY) + if err != nil { + if db.IsNotFound(err) { + logg.InfoCtxf(ctx, "Unregistered number") + + res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) + res.Content = recipient + + return res, nil + } + + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err) return res, nil } } @@ -756,12 +968,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 } @@ -771,6 +983,31 @@ func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byt return res, nil } +// InviteValidRecipient sends an invitation to the valid phone number. +func (h *Handlers) InviteValidRecipient(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") + } + + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + + recipient, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + + // TODO + // send an invitation SMS + // if successful + // res.Content = l.Get("Your invitation to %s to join Sarafu Network has been sent.", string(recipient)) + + res.Content = l.Get("Your invite request for %s to Sarafu Network failed. Please try again later.", string(recipient)) + return res, nil +} + // ResetTransactionAmount resets the transaction amount and invalid flag func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -783,7 +1020,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 } @@ -804,15 +1041,14 @@ 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) - balanceResp, err := h.accountService.CheckBalance(string(publicKey)) + activeBal, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_BAL) if err != nil { - return res, nil + logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", common.DATA_ACTIVE_BAL, "error", err) + return res, err } - balance := balanceResp.Result.Balance - res.Content = balance + res.Content = string(activeBal) return res, nil } @@ -821,54 +1057,31 @@ 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") - flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") - store := h.userdataStore - publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) - amountStr := string(input) + var balanceValue float64 - balanceRes, err := h.accountService.CheckBalance(string(publicKey)) - balanceStr := balanceRes.Result.Balance - - if !balanceRes.Ok { - res.FlagSet = append(res.FlagSet, flag_api_error) - return res, nil - } + // retrieve the active balance + activeBal, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_BAL) if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", common.DATA_ACTIVE_BAL, "error", err) return res, err } - res.Content = balanceStr - res.FlagReset = append(res.FlagReset, flag_api_error) - - // 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) + logg.ErrorCtxf(ctx, "Failed to convert the activeBal to a float", "error", 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 @@ -881,16 +1094,19 @@ 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 { + logg.ErrorCtxf(ctx, "failed to write amount entry with", "key", common.DATA_AMOUNT, "value", formattedAmount, "error", err) return res, err } + res.Content = formattedAmount return res, nil } -// GetRecipient returns the transaction recipient from the gdbm. +// GetRecipient returns the transaction recipient phone number from the gdbm. func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -899,14 +1115,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_TEMPORARY_VALUE) 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 @@ -915,10 +1147,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 = sessionId return res, nil } @@ -932,15 +1161,22 @@ 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) + // retrieve the active symbol + activeSym, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", common.DATA_ACTIVE_SYM, "error", err) + return res, err + } + + amount, _ := store.ReadEntry(ctx, sessionId, common.DATA_AMOUNT) + + res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym)) return res, nil } -// InitiateTransaction returns a confirmation and resets the transaction data -// on the gdbm store. +// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result var err error @@ -949,26 +1185,139 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input [] 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") - // 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) - - recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT) - - res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(publicKey)) - - account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized") + data, err := common.ReadTransactionData(ctx, h.userdataStore, sessionId) if err != nil { return res, err } - res.FlagReset = append(res.FlagReset, account_authorized_flag) + finalAmountStr, err := common.ParseAndScaleAmount(data.Amount, data.ActiveDecimal) + if err != nil { + return res, err + } + + // Call TokenTransfer + r, err := h.accountService.TokenTransfer(ctx, finalAmountStr, data.PublicKey, data.Recipient, data.ActiveAddress) + if err != nil { + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + res.FlagSet = append(res.FlagSet, flag_api_error) + res.Content = l.Get("Your request failed. Please try again later.") + logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err) + return res, nil + } + + trackingId := r.TrackingId + logg.InfoCtxf(ctx, "TokenTransfer", "trackingId", trackingId) + + res.Content = l.Get( + "Your request has been sent. %s will receive %s %s from %s.", + data.TemporaryValue, + data.Amount, + data.ActiveSym, + sessionId, + ) + + res.FlagReset = append(res.FlagReset, flag_account_authorized) + return res, nil +} + +func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var profileInfo []byte + var err error + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + sm, _ := h.st.Where() + parts := strings.SplitN(sm, "_", 2) + filename := parts[1] + dbKeyStr := "DATA_" + strings.ToUpper(filename) + dbKey, err := common.StringToDataTyp(dbKeyStr) + + if err != nil { + return res, err + } + store := h.userdataStore + + switch dbKey { + case common.DATA_FIRST_NAME: + profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) + if err != nil { + if db.IsNotFound(err) { + res.Content = "Not provided" + break + } + logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err) + return res, err + } + res.Content = string(profileInfo) + case common.DATA_FAMILY_NAME: + profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) + if err != nil { + if db.IsNotFound(err) { + res.Content = "Not provided" + break + } + logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err) + return res, err + } + res.Content = string(profileInfo) + + case common.DATA_GENDER: + profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_GENDER) + if err != nil { + if db.IsNotFound(err) { + res.Content = "Not provided" + break + } + logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err) + return res, err + } + res.Content = string(profileInfo) + case common.DATA_YOB: + profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB) + if err != nil { + if db.IsNotFound(err) { + res.Content = "Not provided" + break + } + logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err) + return res, err + } + res.Content = string(profileInfo) + + case common.DATA_LOCATION: + profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) + if err != nil { + if db.IsNotFound(err) { + res.Content = "Not provided" + break + } + logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err) + return res, err + } + res.Content = string(profileInfo) + case common.DATA_OFFERINGS: + profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) + if err != nil { + if db.IsNotFound(err) { + res.Content = "Not provided" + break + } + logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err) + return res, err + } + res.Content = string(profileInfo) + default: + break + } + return res, nil } @@ -999,12 +1348,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 @@ -1046,11 +1395,250 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) return res, nil } -// GetTransactions retrieves the transactions from the API using the "PublicKey" +// 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 { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + // Fetch vouchers from the API using the public key + vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_no_active_voucher) + return res, nil + } + + // 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 + defaultDec := firstVoucher.TokenDecimals + defaultAddr := firstVoucher.ContractAddress + + // Scale down the balance + scaledBalance := common.ScaleDownBalance(defaultBal, defaultDec) + + // set the active symbol + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(defaultSym)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write defaultSym entry with", "key", common.DATA_ACTIVE_SYM, "value", defaultSym, "error", err) + return res, err + } + // set the active balance + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_BAL, []byte(scaledBalance)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write defaultBal entry with", "key", common.DATA_ACTIVE_BAL, "value", scaledBalance, "error", err) + return res, err + } + // set the active decimals + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_DECIMAL, []byte(defaultDec)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write defaultDec entry with", "key", common.DATA_ACTIVE_DECIMAL, "value", defaultDec, "error", err) + return res, err + } + // set the active contract address + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(defaultAddr)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write defaultAddr entry with", "key", common.DATA_ACTIVE_ADDRESS, "value", defaultAddr, "error", err) + return res, err + } + + return res, nil + } + + logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", common.DATA_ACTIVE_SYM, "error", err) + 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 { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) + 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, 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 { + logg.ErrorCtxf(ctx, "Failed to read the voucherData from prefixDb", "error", err) + return res, err + } + + res.Content = string(voucherData) + + return res, nil +} + +// ViewVoucher retrieves the token holding and balance from the subprefixDB +// and displays it to the user for them to select it +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 { + logg.ErrorCtxf(ctx, "failed on StoreTemporaryVoucher", "error", err) + 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 { + logg.ErrorCtxf(ctx, "failed on GetTemporaryVoucherData", "error", err) + return res, err + } + + // Set as active and clear temporary data + if err := common.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil { + logg.ErrorCtxf(ctx, "failed on UpdateVoucherData", "error", err) + return res, err + } + + res.Content = tempData.TokenSymbol + return res, nil +} + +// GetVoucherDetails retrieves the voucher details +func (h *Handlers) GetVoucherDetails(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") + } + + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + + // get the active address + activeAddress, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeAddress entry with", "key", common.DATA_ACTIVE_ADDRESS, "error", err) + return res, err + } + + // use the voucher contract address to get the data from the API + voucherData, err := h.accountService.VoucherData(ctx, string(activeAddress)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + return res, nil + } + + tokenSymbol := voucherData.TokenSymbol + tokenName := voucherData.TokenName + + res.Content = fmt.Sprintf("%s %s", tokenSymbol, tokenName) + + return res, nil +} + +// GetTransactions retrieves the transactions from the API using the "PublicKey" func (h *Handlers) GetTransactions(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - + res.Content = "Transaction list" return res, nil -} \ No newline at end of file +} diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 38c468c..51693df 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -2,26 +2,28 @@ package ussd import ( "context" - "encoding/json" "fmt" "log" "path" "testing" - "time" - "git.defalsify.org/vise.git/asm" - "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/handlers/server" - "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 ( @@ -29,15 +31,46 @@ var ( flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") ) +// 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 := server.TestAccountService{} + accountService := testservice.TestAccountService{} if err != nil { t.Logf(err.Error()) } t.Run("Valid UserDataStore", func(t *testing.T) { - mockStore := &mocks.MockUserDataStore{} - handlers, err := NewHandlers(fm.parser, mockStore, &accountService) + handlers, err := NewHandlers(fm.parser, store, nil, &accountService) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -51,10 +84,7 @@ func TestNewHandlers(t *testing.T) { // Test case for nil userdataStore t.Run("Nil UserDataStore", func(t *testing.T) { - appFlags := &asm.FlagParser{} - - handlers, err := NewHandlers(appFlags, nil, &accountService) - + handlers, err := NewHandlers(fm.parser, nil, nil, &accountService) if err == nil { t.Fatal("expected an error, got none") } @@ -68,72 +98,58 @@ func TestNewHandlers(t *testing.T) { } func TestCreateAccount(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{} - accountCreatedFlag, err := fm.GetFlag("flag_account_created") - + flag_account_created, err := fm.GetFlag("flag_account_created") if err != nil { t.Logf(err.Error()) } - expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag) - // Define session ID and mock data - sessionId := "session123" - typ := utils.DATA_ACCOUNT_CREATED - fakeError := db.ErrNotFound{} - // 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("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", + 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) { @@ -156,20 +172,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 @@ -179,25 +205,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 @@ -207,25 +244,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 { @@ -251,199 +476,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, @@ -452,129 +512,96 @@ 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 := &models.BalanceResponse{ - Ok: true, - } - - // 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.Result.Balance, 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) - recepient := "0xcasgatweksalw1018221" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) - // Set up the expected behavior of the mock - mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return([]byte(recepient), nil) + recepient := "0712345678" + + err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []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) { @@ -592,12 +619,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 @@ -636,25 +662,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 @@ -672,7 +697,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, @@ -680,25 +704,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 @@ -716,7 +739,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, @@ -724,25 +746,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 @@ -760,7 +781,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, @@ -768,25 +788,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 @@ -804,7 +823,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, @@ -812,43 +830,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, } @@ -883,14 +897,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)) @@ -900,33 +910,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, } @@ -961,7 +963,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)) @@ -970,37 +971,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, } @@ -1027,70 +1022,49 @@ 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) { + 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" 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) - tests := []struct { name string - input []byte - response *models.TrackStatusResponse + publicKey []byte + response *models.TrackStatusResult expectedResult resource.Result }{ { - name: "Test when account status is Success", - input: []byte("TrackingId1234"), - response: &models.TrackStatusResponse{ - Ok: true, - 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\"" - } - }{ - Transaction: models.Transaction{ - CreatedAt: time.Now(), - Status: "SUCCESS", - TransferValue: json.Number("0.5"), - TxHash: "0x123abc456def", - TxType: "transfer", - }, - }, + 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}, @@ -1098,37 +1072,10 @@ func TestCheckAccountStatus(t *testing.T) { }, }, { - name: "Test when fetching account status is not Success", - input: []byte("TrackingId1234"), - response: &models.TrackStatusResponse{ - Ok: false, - }, - expectedResult: resource.Result{ - FlagSet: []uint32{flag_api_error}, - }, - }, - { - name: "Test when checking account status api call is a SUCCESS but an account is not yet ready", - input: []byte("TrackingId1234"), - response: &models.TrackStatusResponse{ - Ok: true, - 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\"" - } - }{ - Transaction: models.Transaction{ - CreatedAt: time.Now(), - Status: "IN_NETWORK", - TransferValue: json.Number("0.5"), - TxHash: "0x123abc456def", - TxType: "transfer", - }, - }, + 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}, @@ -1138,58 +1085,51 @@ func TestCheckAccountStatus(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - 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, } - status := tt.response.Result.Transaction.Status - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TRACKING_ID).Return(tt.input, nil) + err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(tt.publicKey)) + if err != nil { + t.Fatal(err) + } - mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe() + mockAccountService.On("TrackAccountStatus", string(tt.publicKey)).Return(tt.response, nil) // Call the method under test - res, _ := h.CheckAccountStatus(ctx, "check_account_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 { @@ -1208,9 +1148,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) @@ -1219,40 +1156,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 }{ { @@ -1264,109 +1193,130 @@ 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 - status string - expectedResult resource.Result + name string + TemporaryValue []byte + ActiveSym []byte + StoredAmount []byte + TransferAmount string + PublicKey []byte + Recipient []byte + ActiveDecimal []byte + ActiveAddress []byte + TransferResponse *models.TokenTransferResponse + expectedResult resource.Result }{ { - name: "Test amount reset", - PublicKey: []byte("0x1241527192"), - Amount: []byte("0.002CELO"), - Recipient: []byte("0x12415ass27192"), + name: "Test initiate transaction", + TemporaryValue: []byte("0711223344"), + ActiveSym: []byte("SRF"), + StoredAmount: []byte("1.00"), + TransferAmount: "1000000", + PublicKey: []byte("0X13242618721"), + Recipient: []byte("0x12415ass27192"), + ActiveDecimal: []byte("6"), + ActiveAddress: []byte("0xd4c288865Ce"), + TransferResponse: &models.TokenTransferResponse{ + TrackingId: "1234567890", + }, 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. 0711223344 will receive 1.00 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_TEMPORARY_VALUE, []byte(tt.TemporaryValue)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(tt.ActiveSym)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.StoredAmount)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(tt.PublicKey)) + 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_DECIMAL, []byte(tt.ActiveDecimal)) + if err != nil { + t.Fatal(err) + } + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tt.ActiveAddress)) + if err != nil { + t.Fatal(err) + } + + mockAccountService.On("TokenTransfer").Return(tt.TransferResponse, nil) // Call the method under test - res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input) + res, _ := h.InitiateTransaction(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 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 { @@ -1395,13 +1345,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 @@ -1457,128 +1404,92 @@ func TestIsValidPIN(t *testing.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") - flag_api_error, _ := fm.GetFlag("flag_api_call_error") - 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 - balanceResponse *models.BalanceResponse - expectedResult resource.Result + name string + input []byte + activeBal []byte + balance string + expectedResult resource.Result }{ { - name: "Test with valid amount", - input: []byte("0.001"), - balanceResponse: &models.BalanceResponse{ - Ok: true, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, - publicKey: []byte("0xrqeqrequuq"), + name: "Test with valid amount", + input: []byte("4.10"), + activeBal: []byte("5"), expectedResult: resource.Result{ - Content: "0.001", - FlagReset: []uint32{flag_api_error}, + Content: "4.10", }, }, { - name: "Test with amount larger than balance", - input: []byte("0.02"), - balanceResponse: &models.BalanceResponse{ - Ok: true, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, - 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}, - FlagReset: []uint32{flag_api_error}, - Content: "0.02", + FlagSet: []uint32{flag_invalid_amount}, + Content: "5.02", }, }, { - name: "Test with invalid amount", - input: []byte("0.02ms"), - balanceResponse: &models.BalanceResponse{ - Ok: true, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, - publicKey: []byte("0xrqeqrequuq"), + name: "Test with invalid amount format", + input: []byte("0.02ms"), + activeBal: []byte("5"), expectedResult: resource.Result{ - FlagSet: []uint32{flag_invalid_amount}, - FlagReset: []uint32{flag_api_error}, - Content: "0.02ms", + FlagSet: []uint32{flag_invalid_amount}, + Content: "0.02ms", }, }, } 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.balanceResponse, 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" + publicKey := "0X13242618721" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + + flag_invalid_recipient, _ := fm.parser.GetFlag("flag_invalid_recipient") + flag_invalid_recipient_with_invite, _ := fm.parser.GetFlag("flag_invalid_recipient_with_invite") + // Define test cases tests := []struct { name string @@ -1587,28 +1498,39 @@ func TestValidateRecipient(t *testing.T) { }{ { name: "Test with invalid recepient", - input: []byte("000"), + input: []byte("9234adf5"), expectedResult: resource.Result{ FlagSet: []uint32{flag_invalid_recipient}, - Content: "000", + Content: "9234adf5", }, }, { - name: "Test with valid recepient", - input: []byte("0705X2"), + name: "Test with valid unregistered recepient", + input: []byte("0712345678"), + expectedResult: resource.Result{ + FlagSet: []uint32{flag_invalid_recipient_with_invite}, + Content: "0712345678", + }, + }, + { + name: "Test with valid registered recepient", + input: []byte("0711223344"), expectedResult: resource.Result{}, }, } + // store a public key for the valid recipient + err = store.WriteEntry(ctx, "0711223344", common.DATA_PUBLIC_KEY, []byte(publicKey)) + if err != nil { + t.Fatal(err) + } + 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 @@ -1617,119 +1539,92 @@ 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) { - - sessionId := "session123" - publicKey := "0X13242618721" - fm, _ := NewFlagManager(flagsPath) - flag_api_error, _ := fm.GetFlag("flag_api_call_error") - - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + ctx, store := InitializeTestStore(t) tests := []struct { name string - balanceResonse *models.BalanceResponse + sessionId string + publicKey string + activeSym string + activeBal string expectedResult resource.Result + expectError bool }{ { - name: "Test when checking a balance is not a success", - balanceResonse: &models.BalanceResponse{ - Ok: false, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, - expectedResult: resource.Result{ - FlagSet: []uint32{flag_api_error}, - }, - }, - { - name: "Test when checking a balance is a success", - balanceResonse: &models.BalanceResponse{ - Ok: true, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, - expectedResult: resource.Result{ - Content: "0.003 CELO", - FlagReset: []uint32{flag_api_error}, - }, + 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, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) + ctx := context.WithValue(ctx, "SessionId", tt.sessionId) - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - mockState := state.NewState(16) - - // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockDataStore, - flagManager: fm.parser, - st: mockState, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, } - // Set up the expected behavior of the mock - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) - mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil) + 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) + } - // Call the method - res, _ := h.CheckBalance(ctx, "check_balance", []byte("")) + res, err := h.CheckBalance(ctx, "check_balance", []byte("")) - // 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") + 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) }) } - } 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, } tests := []struct { name string languageCode string - keys []utils.DataTyp + keys []common.DataTyp profileInfo []string result resource.Result }{ { name: "Test with full profile information in eng", - keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, + 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{ @@ -1740,8 +1635,8 @@ func TestGetProfile(t *testing.T) { }, }, { - name: "Test with with profile information in swa ", - keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, + 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{ @@ -1753,7 +1648,7 @@ func TestGetProfile(t *testing.T) { }, { name: "Test with with profile information with language that is not yet supported", - keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, + 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{ @@ -1766,22 +1661,21 @@ func TestGetProfile(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + 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).Maybe() + 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) - //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") - }) } } @@ -1792,12 +1686,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) @@ -1820,80 +1712,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 @@ -1913,102 +1757,270 @@ 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") +func TestFetchCommunityBalance(t *testing.T) { // Define test data sessionId := "session123" - publicKey := "0X13242618721" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + ctx, store := InitializeTestStore(t) tests := []struct { name string - balanceResonse *models.BalanceResponse + languageCode string expectedResult resource.Result }{ { - name: "Test when fetch custodial balances is not a success", - balanceResonse: &models.BalanceResponse{ - Ok: false, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, + name: "Test community balance content when language is english", expectedResult: resource.Result{ - FlagSet: []uint32{flag_api_error}, - }, - }, - { - name: "Test when fetch custodial balances is a success", - balanceResonse: &models.BalanceResponse{ - Ok: true, - Result: struct { - Balance string `json:"balance"` - Nonce json.Number `json:"nonce"` - }{ - Balance: "0.003 CELO", - Nonce: json.Number("0"), - }, - }, - expectedResult: resource.Result{ - FlagReset: []uint32{flag_api_error}, + Content: "Community Balance: 0.00", }, + languageCode: "eng", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) + mockAccountService := new(mocks.MockAccountService) mockState := state.NewState(16) - // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockDataStore, - flagManager: fm.parser, + userdataStore: store, st: mockState, - accountService: mockCreateAccountService, + accountService: mockAccountService, } - - // Set up the expected behavior of the mock - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) - mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil) + ctx = context.WithValue(ctx, "SessionId", sessionId) + ctx = context.WithValue(ctx, "Language", lang.Language{ + Code: tt.languageCode, + }) // Call the method - res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte("")) - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) + res, _ := h.FetchCommunityBalance(ctx, "fetch_community_balance", []byte("")) //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") - + 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 d828045..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) (*models.BalanceResponse, error) { - args := m.Called(publicKey) - return args.Get(0).(*models.BalanceResponse), args.Error(1) -} - -func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { - args := m.Called(trackingId) - return args.Get(0).(*models.TrackStatusResponse), 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 1629a7c..0000000 --- a/internal/models/trackstatusresponse.go +++ /dev/null @@ -1,27 +0,0 @@ -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 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"` -} 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/testutil/TestEngine.go b/internal/testutil/TestEngine.go index 75ac817..3fcb307 100644 --- a/internal/testutil/TestEngine.go +++ b/internal/testutil/TestEngine.go @@ -11,9 +11,11 @@ import ( "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/handlers/server" "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 ( @@ -71,7 +73,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { 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) @@ -80,16 +82,16 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { os.Exit(1) } - if AccountService == nil { - AccountService = &server.AccountService{} + if testtag.AccountService == nil { + testtag.AccountService = &remote.AccountService{} } - switch AccountService.(type) { - case *server.TestAccountService: + switch testtag.AccountService.(type) { + case *testservice.TestAccountService: go func() { eventChannel <- false }() - case *server.AccountService: + case *remote.AccountService: go func() { time.Sleep(5 * time.Second) // Wait for 5 seconds eventChannel <- true @@ -98,7 +100,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { panic("Unknown account service type") } - hl, err := lhs.GetHandler(AccountService) + hl, err := lhs.GetHandler(testtag.AccountService) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) diff --git a/driver/groupdriver.go b/internal/testutil/driver/groupdriver.go similarity index 100% rename from driver/groupdriver.go rename to internal/testutil/driver/groupdriver.go 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..a25f1ae --- /dev/null +++ b/internal/testutil/mocks/servicemock.go @@ -0,0 +1,49 @@ +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) +} + +func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { + args := m.Called() + return args.Get(0).(*models.TokenTransferResponse), args.Error(1) +} diff --git a/internal/testutil/offlinetest.go b/internal/testutil/offlinetest.go deleted file mode 100644 index 476ade3..0000000 --- a/internal/testutil/offlinetest.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !online - -package testutil - -import ( - "git.grassecon.net/urdt/ussd/internal/handlers/server" -) - -var ( - AccountService server.AccountServiceInterface = &server.TestAccountService{} -) diff --git a/internal/testutil/testservice/TestAccountService.go b/internal/testutil/testservice/TestAccountService.go new file mode 100644 index 0000000..7c486f3 --- /dev/null +++ b/internal/testutil/testservice/TestAccountService.go @@ -0,0 +1,58 @@ +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 +} + +func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { + return &models.TokenTransferResponse{ + TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", + }, 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/onlinetest.go b/internal/testutil/testtag/onlinetest.go similarity index 88% rename from internal/testutil/onlinetest.go rename to internal/testutil/testtag/onlinetest.go index ddb5cf0..92cbb14 100644 --- a/internal/testutil/onlinetest.go +++ b/internal/testutil/testtag/onlinetest.go @@ -1,6 +1,6 @@ // +build online -package testutil +package testtag import "git.grassecon.net/urdt/ussd/internal/handlers/server" 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/internal/utils/db.go b/internal/utils/db.go deleted file mode 100644 index 410da68..0000000 --- a/internal/utils/db.go +++ /dev/null @@ -1,37 +0,0 @@ -package utils - -import ( - "encoding/binary" -) - -type DataTyp uint16 - -const ( - DATA_ACCOUNT DataTyp = iota - DATA_ACCOUNT_CREATED - DATA_TRACKING_ID - DATA_PUBLIC_KEY - DATA_CUSTODIAL_ID - DATA_ACCOUNT_PIN - DATA_ACCOUNT_STATUS - DATA_FIRST_NAME - DATA_FAMILY_NAME - DATA_YOB - DATA_LOCATION - DATA_GENDER - DATA_OFFERINGS - DATA_RECIPIENT - DATA_AMOUNT - DATA_TEMPORARY_PIN -) - -func typToBytes(typ DataTyp) []byte { - var b [2]byte - binary.BigEndian.PutUint16(b[:], uint16(typ)) - return b[:] -} - -func PackKey(typ DataTyp, data []byte) []byte { - v := typToBytes(typ) - return append(v, data...) -} diff --git a/menutraversal_test/group_test.json b/menutraversal_test/group_test.json index 203ff08..52fab59 100644 --- a/menutraversal_test/group_test.json +++ b/menutraversal_test/group_test.json @@ -5,7 +5,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -13,7 +13,7 @@ }, { "input": "5", - "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n3:Guard my PIN\n0:Back" + "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back" }, { "input": "1", @@ -33,7 +33,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -42,7 +42,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -54,7 +54,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" }, { "input": "1", @@ -70,7 +70,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -79,7 +79,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -95,7 +95,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" }, { "input": "1", @@ -103,7 +103,7 @@ }, { "input": "1234", - "expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit" + "expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit" }, { "input": "0", @@ -116,7 +116,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -125,7 +125,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -141,7 +141,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" }, { "input": "1", @@ -149,7 +149,7 @@ }, { "input": "1234", - "expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit" + "expectedContent": "{balance}\n0:Back\n9:Quit" }, { "input": "0", @@ -162,7 +162,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -171,7 +171,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -203,7 +203,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -212,7 +212,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -244,7 +244,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] @@ -254,7 +254,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -286,7 +286,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -295,7 +295,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -327,7 +327,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -336,7 +336,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -368,7 +368,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -377,7 +377,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -409,7 +409,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" } ] }, @@ -418,7 +418,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -446,7 +446,7 @@ }, { "input": "0", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "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 index 5eb1ef8..d79b771 100644 --- a/menutraversal_test/menu_traversal_test.go +++ b/menutraversal_test/menu_traversal_test.go @@ -9,8 +9,8 @@ import ( "regexp" "testing" - "git.grassecon.net/urdt/ussd/driver" "git.grassecon.net/urdt/ussd/internal/testutil" + "git.grassecon.net/urdt/ussd/internal/testutil/driver" "github.com/gofrs/uuid" ) @@ -43,6 +43,39 @@ func extractPublicKey(response []byte) string { 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() { @@ -154,6 +187,12 @@ func TestMainMenuHelp(t *testing.T) { } 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) @@ -189,6 +228,12 @@ func TestMainMenuQuit(t *testing.T) { } 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) @@ -225,8 +270,13 @@ func TestMyAccount_MyAddress(t *testing.T) { } b := w.Bytes() + balance := extractBalance(b) publicKey := extractPublicKey(b) - expectedContent := bytes.Replace([]byte(step.ExpectedContent), []byte("{public_key}"), []byte(publicKey), -1) + + 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 { @@ -240,6 +290,52 @@ func TestMyAccount_MyAddress(t *testing.T) { } } +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 { @@ -265,6 +361,13 @@ func TestGroups(t *testing.T) { 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) @@ -272,7 +375,6 @@ func TestGroups(t *testing.T) { 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 index 56c0278..a554939 100644 --- a/menutraversal_test/test_setup.json +++ b/menutraversal_test/test_setup.json @@ -23,7 +23,7 @@ }, { "input": "1111", - "expectedContent": "The PIN is not a match. Try again\n1:retry\n9:Quit" + "expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit" }, { "input": "1", @@ -53,11 +53,11 @@ ] }, { - "name": "send_with_invalid_inputs", + "name": "send_with_invite", "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "1", @@ -65,39 +65,19 @@ }, { "input": "000", - "expectedContent": "000 is not registered or invalid, please try again:\n1:retry\n9:Quit" + "expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit" }, { "input": "1", "expectedContent": "Enter recipient's phone number:\n0:Back" }, { - "input": "065656", - "expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back" + "input": "0712345678", + "expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit" }, { - "input": "0.1", - "expectedContent": "Amount 0.1 is invalid, please try again:\n1:retry\n9:Quit" - }, - { - "input": "1", - "expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back" - }, - { - "input": "0.001", - "expectedContent": "065656 will receive 0.001 from {public_key}\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 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" - }, - { - "input": "1234", - "expectedContent": "Your request has been sent. 065656 will receive 0.001 from {public_key}." + "input": "2", + "expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later." } ] }, @@ -106,7 +86,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "4", @@ -119,7 +99,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "9", @@ -132,7 +112,7 @@ "steps": [ { "input": "", - "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" }, { "input": "3", @@ -140,7 +120,7 @@ }, { "input": "6", - "expectedContent": "Address: {public_key}\n9:Quit" + "expectedContent": "Address: {public_key}\n0:Back\n9:Quit" }, { "input": "9", diff --git a/models/accountresponse.go b/models/accountresponse.go new file mode 100644 index 0000000..dc8e758 --- /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..88e9ce9 --- /dev/null +++ b/models/balanceresponse.go @@ -0,0 +1,8 @@ +package models + +import "encoding/json" + +type BalanceResult struct { + Balance string `json:"balance"` + Nonce json.Number `json:"nonce"` +} diff --git a/models/token_transfer_response.go b/models/token_transfer_response.go new file mode 100644 index 0000000..b4d6dc3 --- /dev/null +++ b/models/token_transfer_response.go @@ -0,0 +1,5 @@ +package models + +type TokenTransferResponse struct { + TrackingId string `json:"trackingId"` +} diff --git a/models/trackstatusresponse.go b/models/trackstatusresponse.go new file mode 100644 index 0000000..0c3c230 --- /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/voucher_data_result.go b/models/voucher_data_result.go new file mode 100644 index 0000000..c9f0b74 --- /dev/null +++ b/models/voucher_data_result.go @@ -0,0 +1,8 @@ +package models + +type VoucherDataResult struct { + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimals int `json:"tokenDecimals"` + SinkAddress string `json:"sinkAddress"` +} diff --git a/remote/accountservice.go b/remote/accountservice.go new file mode 100644 index 0000000..23b62ca --- /dev/null +++ b/remote/accountservice.go @@ -0,0 +1,273 @@ +package remote + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "log" + "net/http" + "net/url" + + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/models" + "github.com/grassrootseconomics/eth-custodial/pkg/api" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +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) + TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, 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 = doRequest(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 = doRequest(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 = doRequest(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 struct { + Holdings []dataserviceapi.TokenHoldings `json:"holdings"` + } + + 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 = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return r.Holdings, 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 struct { + Transfers []dataserviceapi.Last10TxResponse `json:"transfers"` + } + + 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 = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return r.Transfers, 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 r struct { + TokenDetails models.VoucherDataResult `json:"tokenDetails"` + } + + 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 = doRequest(ctx, req, &r) + return &r.TokenDetails, err +} + +// TokenTransfer creates a new token transfer in the custodial system. +// Returns: +// - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId. +// 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) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { + var r models.TokenTransferResponse + + // Create request payload + payload := map[string]string{ + "amount": amount, + "from": from, + "to": to, + "tokenAddress": tokenAddress, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + // Create a new request + req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { + var okResponse api.OKResponse + var errResponse api.ErrResponse + + req.Header.Set("Authorization", "Bearer "+config.BearerToken) + req.Header.Set("Content-Type", "application/json") + + logRequestDetails(req) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error()) + errResponse.Description = err.Error() + return nil, err + } + defer resp.Body.Close() + + log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type")) + 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") + } + + v, err := json.Marshal(okResponse.Result) + if err != nil { + return nil, err + } + + err = json.Unmarshal(v, &rcpt) + return &okResponse, err +} + +func logRequestDetails(req *http.Request) { + var bodyBytes []byte + contentType := req.Header.Get("Content-Type") + if req.Body != nil { + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + log.Printf("Error reading request body: %s", err) + return + } + req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } else { + bodyBytes = []byte("-") + } + + log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes)) +} 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/address.vis b/services/registration/address.vis index f3ba04a..dfc46d1 100644 --- a/services/registration/address.vis +++ b/services/registration/address.vis @@ -1,6 +1,8 @@ LOAD check_identifier 0 RELOAD check_identifier MAP check_identifier +MOUT back 0 MOUT quit 9 HALT +INCMP _ 0 INCMP quit 9 diff --git a/services/registration/address_swa b/services/registration/address_swa new file mode 100644 index 0000000..3e7a55e --- /dev/null +++ b/services/registration/address_swa @@ -0,0 +1 @@ +Anwani:{{.check_identifier}} \ No newline at end of file diff --git a/services/registration/amount.vis b/services/registration/amount.vis index e87006e..2266160 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -1,14 +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 api_failure flag_api_call_error 1 CATCH invalid_amount flag_invalid_amount 1 INCMP _ 0 -LOAD get_recipient 12 +LOAD get_recipient 0 LOAD get_sender 64 -LOAD get_amount 12 +LOAD get_amount 32 INCMP transaction_pin * diff --git a/services/registration/api_failure.vis b/services/registration/api_failure.vis index e045355..37b3deb 100644 --- a/services/registration/api_failure.vis +++ b/services/registration/api_failure.vis @@ -1,5 +1,5 @@ -MOUT retry 0 +MOUT retry 1 MOUT quit 9 HALT -INCMP _ 0 +INCMP _ 1 INCMP quit 9 diff --git a/services/registration/comminity_balance_swa b/services/registration/comminity_balance_swa index 726fc99..d9f1d6e 100644 --- a/services/registration/comminity_balance_swa +++ b/services/registration/comminity_balance_swa @@ -1 +1 @@ -Salio la kikundi \ No newline at end of file +{{.fetch_community_balance}} \ No newline at end of file diff --git a/services/registration/community_balance b/services/registration/community_balance index f8f8318..d9f1d6e 100644 --- a/services/registration/community_balance +++ b/services/registration/community_balance @@ -1 +1 @@ -{{.fetch_custodial_balances}} \ No newline at end of file +{{.fetch_community_balance}} \ No newline at end of file diff --git a/services/registration/community_balance.vis b/services/registration/community_balance.vis index 85ae93a..f3e0ae1 100644 --- a/services/registration/community_balance.vis +++ b/services/registration/community_balance.vis @@ -1,7 +1,7 @@ LOAD reset_incorrect 6 -LOAD fetch_custodial_balances 0 +LOAD fetch_community_balance 0 CATCH api_failure flag_api_call_error 1 -MAP fetch_custodial_balances +MAP fetch_community_balance CATCH incorrect_pin flag_incorrect_pin 1 CATCH pin_entry flag_account_authorized 0 MOUT back 0 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_others_new_pin_swa b/services/registration/confirm_others_new_pin_swa new file mode 100644 index 0000000..f0b09c8 --- /dev/null +++ b/services/registration/confirm_others_new_pin_swa @@ -0,0 +1 @@ +Tafadhali thibitisha PIN mpya ya: {{.retrieve_blocked_number}} \ No newline at end of file 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_family_name b/services/registration/edit_family_name new file mode 100644 index 0000000..1d637be --- /dev/null +++ b/services/registration/edit_family_name @@ -0,0 +1,2 @@ +Current family name: {{.get_current_profile_info}} +Enter family name: \ No newline at end of file diff --git a/services/registration/enter_familyname.vis b/services/registration/edit_family_name.vis similarity index 55% rename from services/registration/enter_familyname.vis rename to services/registration/edit_family_name.vis index b9fe7b0..1d71939 100644 --- a/services/registration/enter_familyname.vis +++ b/services/registration/edit_family_name.vis @@ -1,9 +1,10 @@ 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 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info MOUT back 0 HALT +LOAD save_familyname 0 RELOAD save_familyname INCMP _ 0 INCMP pin_entry * diff --git a/services/registration/edit_familyname_menu b/services/registration/edit_family_name_menu similarity index 100% rename from services/registration/edit_familyname_menu rename to services/registration/edit_family_name_menu diff --git a/services/registration/edit_familyname_menu_swa b/services/registration/edit_family_name_menu_swa similarity index 100% rename from services/registration/edit_familyname_menu_swa rename to services/registration/edit_family_name_menu_swa diff --git a/services/registration/edit_family_name_swa b/services/registration/edit_family_name_swa new file mode 100644 index 0000000..a1a1cab --- /dev/null +++ b/services/registration/edit_family_name_swa @@ -0,0 +1,2 @@ +Jina la familia la sasa: {{.get_current_profile_info}} +Weka jina la familia \ No newline at end of file diff --git a/services/registration/edit_first_name b/services/registration/edit_first_name new file mode 100644 index 0000000..3d141ee --- /dev/null +++ b/services/registration/edit_first_name @@ -0,0 +1,2 @@ +Current name: {{.get_current_profile_info}} +Enter your first names: \ No newline at end of file diff --git a/services/registration/edit_first_name.vis b/services/registration/edit_first_name.vis new file mode 100644 index 0000000..b8dab5a --- /dev/null +++ b/services/registration/edit_first_name.vis @@ -0,0 +1,11 @@ +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH update_firstname flag_allow_update 1 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info +MAP get_current_profile_info +MOUT back 0 +HALT +LOAD save_firstname 0 +RELOAD save_firstname +INCMP _ 0 +INCMP pin_entry * diff --git a/services/registration/edit_name_menu b/services/registration/edit_first_name_menu similarity index 100% rename from services/registration/edit_name_menu rename to services/registration/edit_first_name_menu diff --git a/services/registration/edit_name_menu_swa b/services/registration/edit_first_name_menu_swa similarity index 100% rename from services/registration/edit_name_menu_swa rename to services/registration/edit_first_name_menu_swa diff --git a/services/registration/edit_first_name_swa b/services/registration/edit_first_name_swa new file mode 100644 index 0000000..3fdd986 --- /dev/null +++ b/services/registration/edit_first_name_swa @@ -0,0 +1,2 @@ +Jina la kwanza la sasa {{.get_current_profile_info}} +Weka majina yako ya kwanza: \ No newline at end of file diff --git a/services/registration/edit_location b/services/registration/edit_location new file mode 100644 index 0000000..4e11d1a --- /dev/null +++ b/services/registration/edit_location @@ -0,0 +1,2 @@ +Current location: {{.get_current_profile_info}} +Enter your location: \ No newline at end of file diff --git a/services/registration/enter_location.vis b/services/registration/edit_location.vis similarity index 54% rename from services/registration/enter_location.vis rename to services/registration/edit_location.vis index fdd29ce..eaf248a 100644 --- a/services/registration/enter_location.vis +++ b/services/registration/edit_location.vis @@ -1,8 +1,10 @@ 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 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info MOUT back 0 HALT +LOAD save_location 0 RELOAD save_location INCMP _ 0 INCMP pin_entry * diff --git a/services/registration/edit_location_swa b/services/registration/edit_location_swa new file mode 100644 index 0000000..0a3476e --- /dev/null +++ b/services/registration/edit_location_swa @@ -0,0 +1,2 @@ +Eneo la sasa {{.get_current_profile_info}} +Weka eneo: \ No newline at end of file diff --git a/services/registration/edit_offerings b/services/registration/edit_offerings new file mode 100644 index 0000000..5bb0e7f --- /dev/null +++ b/services/registration/edit_offerings @@ -0,0 +1,2 @@ +Current offerings: {{.get_current_profile_info}} +Enter the services or goods you offer: \ No newline at end of file diff --git a/services/registration/enter_offerings.vis b/services/registration/edit_offerings.vis similarity index 54% rename from services/registration/enter_offerings.vis rename to services/registration/edit_offerings.vis index 26e4b61..dad3193 100644 --- a/services/registration/enter_offerings.vis +++ b/services/registration/edit_offerings.vis @@ -1,5 +1,7 @@ CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 +CATCH update_offerings flag_allow_update 1 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info LOAD save_offerings 0 MOUT back 0 HALT diff --git a/services/registration/edit_offerings_swa b/services/registration/edit_offerings_swa new file mode 100644 index 0000000..cd81497 --- /dev/null +++ b/services/registration/edit_offerings_swa @@ -0,0 +1,2 @@ +Unachouza kwa sasa: {{.get_current_profile_info}} +Weka unachouza \ No newline at end of file diff --git a/services/registration/edit_profile.vis b/services/registration/edit_profile.vis index 277f330..af20e0f 100644 --- a/services/registration/edit_profile.vis +++ b/services/registration/edit_profile.vis @@ -2,8 +2,8 @@ LOAD reset_account_authorized 16 RELOAD reset_account_authorized LOAD reset_allow_update 0 RELOAD reset_allow_update -MOUT edit_name 1 -MOUT edit_familyname 2 +MOUT edit_first_name 1 +MOUT edit_family_name 2 MOUT edit_gender 3 MOUT edit_yob 4 MOUT edit_location 5 @@ -12,10 +12,10 @@ MOUT view 7 MOUT back 0 HALT INCMP my_account 0 -INCMP enter_name 1 -INCMP enter_familyname 2 +INCMP edit_first_name 1 +INCMP edit_family_name 2 INCMP select_gender 3 -INCMP enter_yob 4 -INCMP enter_location 5 -INCMP enter_offerings 6 +INCMP edit_yob 4 +INCMP edit_location 5 +INCMP edit_offerings 6 INCMP view_profile 7 diff --git a/services/registration/edit_yob b/services/registration/edit_yob new file mode 100644 index 0000000..105812b --- /dev/null +++ b/services/registration/edit_yob @@ -0,0 +1,2 @@ +Current year of birth: {{.get_current_profile_info}} +Enter your year of birth \ No newline at end of file diff --git a/services/registration/enter_yob.vis b/services/registration/edit_yob.vis similarity index 51% rename from services/registration/enter_yob.vis rename to services/registration/edit_yob.vis index 40bf3f4..6a5abe0 100644 --- a/services/registration/enter_yob.vis +++ b/services/registration/edit_yob.vis @@ -1,10 +1,14 @@ 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 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info +MAP get_current_profile_info MOUT back 0 HALT -LOAD verify_yob 0 +LOAD verify_yob 6 +RELOAD verify_yob 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/edit_yob_swa b/services/registration/edit_yob_swa new file mode 100644 index 0000000..e0b5708 --- /dev/null +++ b/services/registration/edit_yob_swa @@ -0,0 +1,2 @@ +Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}} +Weka mwaka wa kuzaliwa \ No newline at end of file diff --git a/services/registration/enter_familyname b/services/registration/enter_familyname deleted file mode 100644 index 889915a..0000000 --- a/services/registration/enter_familyname +++ /dev/null @@ -1 +0,0 @@ -Enter family name: \ No newline at end of file diff --git a/services/registration/enter_familyname_swa b/services/registration/enter_familyname_swa deleted file mode 100644 index 82f64cd..0000000 --- a/services/registration/enter_familyname_swa +++ /dev/null @@ -1 +0,0 @@ -Weka jina la familia diff --git a/services/registration/enter_location b/services/registration/enter_location deleted file mode 100644 index 675b835..0000000 --- a/services/registration/enter_location +++ /dev/null @@ -1 +0,0 @@ -Enter your location: \ No newline at end of file diff --git a/services/registration/enter_location_swa b/services/registration/enter_location_swa deleted file mode 100644 index 41682a2..0000000 --- a/services/registration/enter_location_swa +++ /dev/null @@ -1 +0,0 @@ -Weka eneo: \ No newline at end of file diff --git a/services/registration/enter_name b/services/registration/enter_name deleted file mode 100644 index c6851cf..0000000 --- a/services/registration/enter_name +++ /dev/null @@ -1 +0,0 @@ -Enter your first names: \ No newline at end of file diff --git a/services/registration/enter_name.vis b/services/registration/enter_name.vis deleted file mode 100644 index 563577e..0000000 --- a/services/registration/enter_name.vis +++ /dev/null @@ -1,12 +0,0 @@ -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH profile_update_success flag_allow_update 1 -LOAD save_firstname 0 -RELOAD save_firstname -MOUT back 0 -HALT -RELOAD save_firstname -INCMP _ 0 -INCMP pin_entry * - - - diff --git a/services/registration/enter_name_swa b/services/registration/enter_name_swa deleted file mode 100644 index b600b90..0000000 --- a/services/registration/enter_name_swa +++ /dev/null @@ -1 +0,0 @@ -Weka majina yako ya kwanza: \ No newline at end of file diff --git a/services/registration/enter_offerings b/services/registration/enter_offerings deleted file mode 100644 index a9333ba..0000000 --- a/services/registration/enter_offerings +++ /dev/null @@ -1 +0,0 @@ -Enter the services or goods you offer: \ No newline at end of file diff --git a/services/registration/enter_offerings_swa b/services/registration/enter_offerings_swa deleted file mode 100644 index f37e125..0000000 --- a/services/registration/enter_offerings_swa +++ /dev/null @@ -1 +0,0 @@ -Weka unachouza \ No newline at end of file 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_other_number_swa b/services/registration/enter_other_number_swa new file mode 100644 index 0000000..214fc4a --- /dev/null +++ b/services/registration/enter_other_number_swa @@ -0,0 +1 @@ +Weka nambari ya simu ili kutuma ombi la kubadilisha nambari ya siri: \ No newline at end of file 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_others_new_pin_swa b/services/registration/enter_others_new_pin_swa new file mode 100644 index 0000000..77ec2f3 --- /dev/null +++ b/services/registration/enter_others_new_pin_swa @@ -0,0 +1 @@ +Tafadhali weka PIN mpya ya: {{.retrieve_blocked_number}} \ No newline at end of file diff --git a/services/registration/enter_yob b/services/registration/enter_yob deleted file mode 100644 index 54e039e..0000000 --- a/services/registration/enter_yob +++ /dev/null @@ -1 +0,0 @@ -Enter your year of birth \ No newline at end of file diff --git a/services/registration/enter_yob_swa b/services/registration/enter_yob_swa deleted file mode 100644 index 9bb272a..0000000 --- a/services/registration/enter_yob_swa +++ /dev/null @@ -1 +0,0 @@ -Weka mwaka wa kuzaliwa \ No newline at end of file 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/incorrect_date_format.vis b/services/registration/incorrect_date_format.vis index e94db5d..f4a8a2b 100644 --- a/services/registration/incorrect_date_format.vis +++ b/services/registration/incorrect_date_format.vis @@ -2,5 +2,5 @@ LOAD reset_incorrect_date_format 8 MOUT retry 1 MOUT quit 9 HALT -INCMP enter_yob 1 +INCMP _ 1 INCMP quit 9 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/invalid_recipient b/services/registration/invalid_recipient index 0be78bd..d9fcb1d 100644 --- a/services/registration/invalid_recipient +++ b/services/registration/invalid_recipient @@ -1 +1 @@ -{{.validate_recipient}} is not registered or invalid, please try again: \ No newline at end of file +{{.validate_recipient}} is invalid, please try again: \ No newline at end of file diff --git a/services/registration/invalid_recipient_swa b/services/registration/invalid_recipient_swa index 39e7804..13dda97 100644 --- a/services/registration/invalid_recipient_swa +++ b/services/registration/invalid_recipient_swa @@ -1 +1 @@ -{{.validate_recipient}} haijasajiliwa au sio sahihi, tafadhali weka tena: \ No newline at end of file +{{.validate_recipient}} sio sahihi, tafadhali weka tena: \ No newline at end of file diff --git a/services/registration/invite_menu b/services/registration/invite_menu new file mode 100644 index 0000000..e44862a --- /dev/null +++ b/services/registration/invite_menu @@ -0,0 +1 @@ +Invite to Sarafu Network \ No newline at end of file diff --git a/services/registration/invite_menu_swa b/services/registration/invite_menu_swa new file mode 100644 index 0000000..48c0ddf --- /dev/null +++ b/services/registration/invite_menu_swa @@ -0,0 +1 @@ +Karibisha kwa matandao wa Sarafu \ No newline at end of file diff --git a/services/registration/invite_recipient b/services/registration/invite_recipient new file mode 100644 index 0000000..aa3438d --- /dev/null +++ b/services/registration/invite_recipient @@ -0,0 +1 @@ +{{.validate_recipient}} is not registered, please try again: \ No newline at end of file diff --git a/services/registration/invite_recipient.vis b/services/registration/invite_recipient.vis new file mode 100644 index 0000000..1a4845f --- /dev/null +++ b/services/registration/invite_recipient.vis @@ -0,0 +1,8 @@ +MAP validate_recipient +MOUT retry 1 +MOUT invite 2 +MOUT quit 9 +HALT +INCMP _ 1 +INCMP invite_result 2 +INCMP quit 9 diff --git a/services/registration/invite_recipient_swa b/services/registration/invite_recipient_swa new file mode 100644 index 0000000..30cf599 --- /dev/null +++ b/services/registration/invite_recipient_swa @@ -0,0 +1 @@ +{{.validate_recipient}} haijasajiliwa, tafadhali weka tena: \ No newline at end of file diff --git a/services/registration/invite_result.vis b/services/registration/invite_result.vis new file mode 100644 index 0000000..5f31749 --- /dev/null +++ b/services/registration/invite_result.vis @@ -0,0 +1,2 @@ +LOAD invite_valid_recipient 0 +HALT diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po index 5289dd7..aa033b5 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -1,12 +1,26 @@ 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" + +msid "Your invite request for %s to Sarafu Network failed. Please try again later." +msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu halikufaulu. Tafadhali jaribu tena baadaye." + +msgid "Your invitation to %s to join Sarafu Network has been sent." +msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu limetumwa." + +msgid "Your request failed. Please try again later." +msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye." + +msgid "Community Balance: 0.00" +msgid "Salio la Kikundi: 0.00" 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 0379078..7e1c9bf 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -1,5 +1,9 @@ +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 @@ -9,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 f8f8318..afae8c1 100644 --- a/services/registration/my_balance +++ b/services/registration/my_balance @@ -1 +1 @@ -{{.fetch_custodial_balances}} \ No newline at end of file +{{.check_balance}} \ No newline at end of file diff --git a/services/registration/my_balance.vis b/services/registration/my_balance.vis index 85ae93a..9144da9 100644 --- a/services/registration/my_balance.vis +++ b/services/registration/my_balance.vis @@ -1,7 +1,7 @@ LOAD reset_incorrect 6 -LOAD fetch_custodial_balances 0 +LOAD check_balance 0 CATCH api_failure flag_api_call_error 1 -MAP fetch_custodial_balances +MAP check_balance CATCH incorrect_pin flag_incorrect_pin 1 CATCH pin_entry flag_account_authorized 0 MOUT back 0 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..e79438e --- /dev/null +++ b/services/registration/my_vouchers.vis @@ -0,0 +1,9 @@ +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 +INCMP voucher_details 2 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_admin_privilege_swa b/services/registration/no_admin_privilege_swa new file mode 100644 index 0000000..6c6d3dc --- /dev/null +++ b/services/registration/no_admin_privilege_swa @@ -0,0 +1 @@ +Huna mapendeleo ya kufanya kitendo hiki \ No newline at end of file diff --git a/services/registration/no_voucher b/services/registration/no_voucher new file mode 100644 index 0000000..6303197 --- /dev/null +++ b/services/registration/no_voucher @@ -0,0 +1 @@ +You need a voucher to proceed \ 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..7291650 --- /dev/null +++ b/services/registration/no_voucher_swa @@ -0,0 +1 @@ +Unahitaji sarafu kuendelea \ 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/others_pin_mismatch_swa b/services/registration/others_pin_mismatch_swa new file mode 100644 index 0000000..5787790 --- /dev/null +++ b/services/registration/others_pin_mismatch_swa @@ -0,0 +1 @@ +PIN uliyoweka hailingani.Jaribu tena. \ No newline at end of file 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_mismatch_swa b/services/registration/pin_reset_mismatch_swa new file mode 100644 index 0000000..5787790 --- /dev/null +++ b/services/registration/pin_reset_mismatch_swa @@ -0,0 +1 @@ +PIN uliyoweka hailingani.Jaribu tena. \ No newline at end of file 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_result_swa b/services/registration/pin_reset_result_swa new file mode 100644 index 0000000..30de04e --- /dev/null +++ b/services/registration/pin_reset_result_swa @@ -0,0 +1 @@ +Ombi la kuweka upya PIN ya {{.retrieve_blocked_number}} limefanikiwa \ No newline at end of file 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 af5b3dc..406cc22 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -14,4 +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_api_call_error,25,this is set when communication to an external service fails +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/retry_menu b/services/registration/retry_menu new file mode 100644 index 0000000..ffde86c --- /dev/null +++ b/services/registration/retry_menu @@ -0,0 +1 @@ +Retry \ No newline at end of file diff --git a/services/registration/retry_menu_swa b/services/registration/retry_menu_swa new file mode 100644 index 0000000..c43b419 --- /dev/null +++ b/services/registration/retry_menu_swa @@ -0,0 +1 @@ +Jaribu tena \ No newline at end of file diff --git a/services/registration/select_gender b/services/registration/select_gender index f8a6f47..22b9be9 100644 --- a/services/registration/select_gender +++ b/services/registration/select_gender @@ -1 +1,2 @@ +Current gender: {{.get_current_profile_info}} Select gender: \ No newline at end of file diff --git a/services/registration/select_gender.vis b/services/registration/select_gender.vis index 1082cef..c1a00f5 100644 --- a/services/registration/select_gender.vis +++ b/services/registration/select_gender.vis @@ -1,5 +1,7 @@ CATCH incorrect_pin flag_incorrect_pin 1 CATCH profile_update_success flag_allow_update 1 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info MOUT male 1 MOUT female 2 MOUT unspecified 3 @@ -9,7 +11,3 @@ INCMP _ 0 INCMP set_male 1 INCMP set_female 2 INCMP set_unspecified 3 - - - - diff --git a/services/registration/select_gender_swa b/services/registration/select_gender_swa index 2b3a748..b077a0b 100644 --- a/services/registration/select_gender_swa +++ b/services/registration/select_gender_swa @@ -1 +1,2 @@ +Jinsia ya sasa {{.get_current_profile_info}} Chagua jinsia \ No newline at end of file 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..058d791 --- /dev/null +++ b/services/registration/select_voucher.vis @@ -0,0 +1,16 @@ +CATCH no_voucher flag_no_active_voucher 1 +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_menu_swa b/services/registration/select_voucher_menu_swa new file mode 100644 index 0000000..2cb4daf --- /dev/null +++ b/services/registration/select_voucher_menu_swa @@ -0,0 +1 @@ +Chagua Sarafu \ 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..8928725 100644 --- a/services/registration/send.vis +++ b/services/registration/send.vis @@ -1,8 +1,11 @@ LOAD transaction_reset 0 +RELOAD transaction_reset +CATCH no_voucher flag_no_active_voucher 1 MOUT back 0 HALT LOAD validate_recipient 20 RELOAD validate_recipient CATCH invalid_recipient flag_invalid_recipient 1 +CATCH invite_recipient flag_invalid_recipient_with_invite 1 INCMP _ 0 INCMP amount * 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/unregistered_number_swa b/services/registration/unregistered_number_swa new file mode 100644 index 0000000..19810cb --- /dev/null +++ b/services/registration/unregistered_number_swa @@ -0,0 +1 @@ +Nambari uliyoingiza haijasajiliwa na Sarafu au sio sahihi. \ No newline at end of file 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_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 b/services/registration/voucher_details new file mode 100644 index 0000000..d437681 --- /dev/null +++ b/services/registration/voucher_details @@ -0,0 +1 @@ +{{.get_voucher_details}} \ No newline at end of file diff --git a/services/registration/voucher_details.vis b/services/registration/voucher_details.vis new file mode 100644 index 0000000..1b009f1 --- /dev/null +++ b/services/registration/voucher_details.vis @@ -0,0 +1,6 @@ +CATCH no_voucher flag_no_active_voucher 1 +LOAD get_voucher_details 0 +MAP get_voucher_details +MOUT back 0 +HALT +INCMP _ 0 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_details_menu_swa b/services/registration/voucher_details_menu_swa new file mode 100644 index 0000000..e84661b --- /dev/null +++ b/services/registration/voucher_details_menu_swa @@ -0,0 +1 @@ +Maelezo ya Sarafu \ No newline at end of file diff --git a/services/registration/voucher_details_swa b/services/registration/voucher_details_swa new file mode 100644 index 0000000..d437681 --- /dev/null +++ b/services/registration/voucher_details_swa @@ -0,0 +1 @@ +{{.get_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