diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a118f64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +/** +!/cmd/africastalking +!/common +!/config +!/initializers +!/internal +!/models +!/remote +!/services +!/LICENSE +!/README.md +!/go.* +!/.env.example \ No newline at end of file diff --git a/.env.example b/.env.example index ab370a7..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,7 +15,6 @@ DB_SSLMODE=disable DB_TIMEZONE=Africa/Nairobi #External API Calls -CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create -TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/ -BALANCE_URL=https://custodial.sarafu.africa/api/account/status/ -TRACK_URL=http://localhost:5003/api/v2/account/status +CUSTODIAL_URL_BASE=http://localhost:5003 +BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr +DATA_URL_BASE=http://localhost:5006 diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..da35100 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,56 @@ +name: release + +on: + push: + tags: + - "v*" + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Check out repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to GHCR Docker registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set outputs + run: | + echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV \ + && echo "RELEASE_SHORT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Build and push image + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + platforms: linux/amd64 + push: true + build-args: | + BUILD=${{ env.RELEASE_SHORT_COMMIT }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + tags: | + ghcr.io/grassrootseconomics/urdt-ussd:latest + ghcr.io/grassrootseconomics/urdt-ussd:${{ env.RELEASE_TAG }} 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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3a5da7d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM golang:1.23.0-bookworm AS build + +ENV CGO_ENABLED=1 + +ARG BUILDPLATFORM +ARG TARGETPLATFORM +ARG BUILD=dev + +WORKDIR /build +COPY . . +RUN apt update && apt install libgdbm-dev +RUN git clone https://git.defalsify.org/vise.git go-vise + +WORKDIR /build/services/registration +RUN echo "Compiling go-vise files" +RUN make VISE_PATH=/build/go-vise -B + +WORKDIR /build +RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM" +RUN go mod download +RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go + +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && apt install libgdbm-dev ca-certificates -y +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +WORKDIR /service + +COPY --from=build /build/ussd-africastalking . +COPY --from=build /build/LICENSE . +COPY --from=build /build/README.md . +COPY --from=build /build/services ./services +COPY --from=build /build/.env.example . +RUN mv .env.example .env + +EXPOSE 7123 + +CMD ["./ussd-africastalking"] \ No newline at end of file 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..98864db 100644 --- a/cmd/africastalking/main.go +++ b/cmd/africastalking/main.go @@ -1,9 +1,12 @@ package main import ( + "bytes" "context" + "encoding/json" "flag" "fmt" + "io" "net/http" "os" "os/signal" @@ -16,17 +19,20 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.grassecon.net/urdt/ussd/common" "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") + + build = "dev" ) func init() { @@ -38,9 +44,30 @@ type atRequestParser struct{} func (arp *atRequestParser) GetSessionId(rq any) (string, error) { rqv, ok := rq.(*http.Request) if !ok { + logg.Warnf("got an invalid request", "req", rq) return "", handlers.ErrInvalidRequest } + + // Capture body (if any) for logging + body, err := io.ReadAll(rqv.Body) + if err != nil { + logg.Warnf("failed to read request body", "err", 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 { + logg.Warnf("failed to marshal request body", "err", err) + } else { + logg.Debugf("received request", "bytes", logBytes) + } + if err := rqv.ParseForm(); err != nil { + logg.Warnf("failed to parse form data", "err", err) return "", fmt.Errorf("failed to parse form data: %v", err) } @@ -49,7 +76,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) { return "", fmt.Errorf("no phone number found") } - return phoneNumber, nil + formattedNumber, err := common.FormatPhoneNumber(phoneNumber) + if err != nil { + logg.Warnf("failed to format phone number", "err", err) + return "", fmt.Errorf("failed to format number") + } + + return formattedNumber, nil } func (arp *atRequestParser) GetInput(rq any) ([]byte, error) { @@ -90,7 +123,7 @@ func main() { flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") flag.Parse() - logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) + logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) @@ -131,7 +164,11 @@ func main() { os.Exit(1) } - lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) + lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } lhs.SetDataStore(&userdataStore) if err != nil { @@ -139,7 +176,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 +193,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..a5cf1c1 --- /dev/null +++ b/common/db.go @@ -0,0 +1,130 @@ +package common + +import ( + "encoding/binary" + "errors" + + "git.defalsify.org/vise.git/logging" +) + +// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA. +// +// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id. +// +// * The first byte is vise/db.DATATYPE_USERDATA +// * The last 2 bytes are the DataTyp value, big-endian. +// * The intermediate bytes are the id of the user context. +// +// All values are strings +type DataTyp uint16 + +const ( + // API Tracking id to follow status of account creation + DATA_TRACKING_ID = iota + // EVM address returned from API on account creation + DATA_PUBLIC_KEY + // Currently active PIN used to authenticate ussd state change requests + DATA_ACCOUNT_PIN + // The first name of the user + DATA_FIRST_NAME + // The last name of the user + DATA_FAMILY_NAME + // The year-of-birth of the user + DATA_YOB + // The location of the user + DATA_LOCATION + // The gender of the user + DATA_GENDER + // The offerings description of the user + DATA_OFFERINGS + // The ethereum address of the recipient of an ongoing send request + DATA_RECIPIENT + // The voucher value amount of an ongoing send request + DATA_AMOUNT + // A general swap field for temporary values + DATA_TEMPORARY_VALUE + // Currently active voucher symbol of user + DATA_ACTIVE_SYM + // Voucher balance of user's currently active voucher + DATA_ACTIVE_BAL + // String boolean indicating whether use of PIN is blocked + DATA_BLOCKED_NUMBER + // Reverse mapping of a user's evm address to a session id. + DATA_PUBLIC_KEY_REVERSE + // Decimal count of the currently active voucher + DATA_ACTIVE_DECIMAL + // EVM address of the currently active voucher + DATA_ACTIVE_ADDRESS +) + +const ( + // List of valid voucher symbols in the user context. + DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota + // List of voucher balances for vouchers valid in the user context. + DATA_VOUCHER_BALANCES + // List of voucher decimal counts for vouchers valid in the user context. + DATA_VOUCHER_DECIMALS + // List of voucher EVM addresses for vouchers valid in the user context. + DATA_VOUCHER_ADDRESSES + // List of senders for valid transactions in the user context. +) + +const ( + DATA_TX_SENDERS = 512 + iota + // List of recipients for valid transactions in the user context. + DATA_TX_RECIPIENTS + // List of voucher values for valid transactions in the user context. + DATA_TX_VALUES + // List of voucher EVM addresses for valid transactions in the user context. + DATA_TX_ADDRESSES + // List of valid transaction hashes in the user context. + DATA_TX_HASHES + // List of transaction dates for valid transactions in the user context. + DATA_TX_DATES + // List of voucher symbols for valid transactions in the user context. + DATA_TX_SYMBOLS + // List of voucher decimal counts for valid transactions in the user context. + DATA_TX_DECIMALS +) + +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") + } +} + +// ToBytes converts DataTyp or int to a byte slice +func ToBytes[T ~uint16 | int](value T) []byte { + bytes := make([]byte, 2) + binary.BigEndian.PutUint16(bytes, uint16(value)) + return bytes +} diff --git a/common/hex.go b/common/hex.go index f5aa7ed..971ecf1 100644 --- a/common/hex.go +++ b/common/hex.go @@ -2,6 +2,7 @@ package common import ( "encoding/hex" + "strings" ) func NormalizeHex(s string) (string, error) { @@ -16,3 +17,15 @@ func NormalizeHex(s string) (string, error) { } 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/recipient.go b/common/recipient.go new file mode 100644 index 0000000..f463a32 --- /dev/null +++ b/common/recipient.go @@ -0,0 +1,73 @@ +package common + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +// Define the regex patterns as constants +const ( + phoneRegex = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$` + addressRegex = `^0x[a-fA-F0-9]{40}$` + aliasRegex = `^[a-zA-Z0-9]+$` +) + +// IsValidPhoneNumber checks if the given number is a valid phone number +func IsValidPhoneNumber(phonenumber string) bool { + match, _ := regexp.MatchString(phoneRegex, phonenumber) + return match +} + +// IsValidAddress checks if the given address is a valid Ethereum address +func IsValidAddress(address string) bool { + match, _ := regexp.MatchString(addressRegex, address) + return match +} + +// IsValidAlias checks if the alias is a valid alias format +func IsValidAlias(alias string) bool { + match, _ := regexp.MatchString(aliasRegex, alias) + return match +} + +// CheckRecipient validates the recipient format based on the criteria +func CheckRecipient(recipient string) (string, error) { + if IsValidPhoneNumber(recipient) { + return "phone number", nil + } + + if IsValidAddress(recipient) { + return "address", nil + } + + if IsValidAlias(recipient) { + return "alias", nil + } + + return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias") +} + +// FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx". +func FormatPhoneNumber(phone string) (string, error) { + if !IsValidPhoneNumber(phone) { + return "", errors.New("invalid phone number") + } + + // Remove any leading "+" and spaces + phone = strings.TrimPrefix(phone, "+") + phone = strings.ReplaceAll(phone, " ", "") + + // Replace leading "0" with "254" if present + if strings.HasPrefix(phone, "0") { + phone = "254" + phone[1:] + } + + // Add "+" if not already present + if !strings.HasPrefix(phone, "254") { + return "", errors.New("unexpected format") + } + + return "+" + phone, nil +} 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/common/transfer_statements.go b/common/transfer_statements.go new file mode 100644 index 0000000..4e6f66b --- /dev/null +++ b/common/transfer_statements.go @@ -0,0 +1,119 @@ +package common + +import ( + "context" + "fmt" + "strings" + "time" + + "git.grassecon.net/urdt/ussd/internal/storage" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// TransferMetadata helps organize data fields +type TransferMetadata struct { + Senders string + Recipients string + TransferValues string + Addresses string + TxHashes string + Dates string + Symbols string + Decimals string +} + +// ProcessTransfers converts transfers into formatted strings +func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata { + var data TransferMetadata + var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string + + for _, t := range transfers { + senders = append(senders, t.Sender) + recipients = append(recipients, t.Recipient) + + // Scale down the amount + scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals) + transferValues = append(transferValues, scaledBalance) + + addresses = append(addresses, t.ContractAddress) + txHashes = append(txHashes, t.TxHash) + dates = append(dates, fmt.Sprintf("%s", t.DateBlock)) + symbols = append(symbols, t.TokenSymbol) + decimals = append(decimals, t.TokenDecimals) + } + + data.Senders = strings.Join(senders, "\n") + data.Recipients = strings.Join(recipients, "\n") + data.TransferValues = strings.Join(transferValues, "\n") + data.Addresses = strings.Join(addresses, "\n") + data.TxHashes = strings.Join(txHashes, "\n") + data.Dates = strings.Join(dates, "\n") + data.Symbols = strings.Join(symbols, "\n") + data.Decimals = strings.Join(decimals, "\n") + + return data +} + +// GetTransferData retrieves and matches transfer data +// returns a formatted string of the full transaction/statement +func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) { + keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS} + data := make(map[DataTyp]string) + + for _, key := range keys { + value, err := db.Get(ctx, ToBytes(key)) + if err != nil { + return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err) + } + data[key] = string(value) + } + + // Split the data + senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n") + recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n") + values := strings.Split(string(data[DATA_TX_VALUES]), "\n") + addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n") + hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n") + dates := strings.Split(string(data[DATA_TX_DATES]), "\n") + syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n") + + // Check if index is within range + if index < 1 || index > len(senders) { + return "", fmt.Errorf("transaction not found: index %d out of range", index) + } + + // Adjust for 0-based indexing + i := index - 1 + transactionType := "received" + party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i])) + if strings.TrimSpace(senders[i]) == publicKey { + transactionType = "sent" + party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i])) + } + + formattedDate := formatDate(strings.TrimSpace(dates[i])) + + // Build the full transaction detail + detail := fmt.Sprintf( + "%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s", + transactionType, + strings.TrimSpace(values[i]), + strings.TrimSpace(syms[i]), + party, + strings.TrimSpace(addresses[i]), + strings.TrimSpace(hashes[i]), + formattedDate, + ) + + return detail, nil +} + +// Helper function to format date in desired output +func formatDate(dateStr string) string { + parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr) + if err != nil { + fmt.Println("Error parsing date:", err) + return "" + } + return parsedDate.Format("2006-01-02 03:04:05 PM") +} diff --git a/internal/utils/userStore.go b/common/user_store.go similarity index 78% rename from internal/utils/userStore.go rename to common/user_store.go index a1485b1..6c770d8 100644 --- a/internal/utils/userStore.go +++ b/common/user_store.go @@ -1,4 +1,4 @@ -package utils +package common import ( "context" @@ -16,17 +16,19 @@ 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) - k := PackKey(typ, []byte(sessionId)) + k := ToBytes(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) - k := PackKey(typ, []byte(sessionId)) + k := ToBytes(typ) return store.Put(ctx, k, value) } diff --git a/internal/utils/vouchers.go b/common/vouchers.go similarity index 64% rename from internal/utils/vouchers.go rename to common/vouchers.go index b027cc1..6cff91d 100644 --- a/internal/utils/vouchers.go +++ b/common/vouchers.go @@ -1,8 +1,9 @@ -package utils +package common import ( "context" "fmt" + "math/big" "strings" "git.grassecon.net/urdt/ussd/internal/storage" @@ -24,7 +25,11 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { for i, h := range holdings { symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) - balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance)) + + // 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)) } @@ -37,24 +42,45 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { 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) + keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES} + data := make(map[DataTyp]string) for _, key := range keys { - value, err := db.Get(ctx, []byte(key)) + value, err := db.Get(ctx, ToBytes(key)) if err != nil { - return nil, fmt.Errorf("failed to get %s: %v", key, err) + return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err) } data[key] = string(value) } symbol, balance, decimal, address := MatchVoucher(input, - data["sym"], - data["bal"], - data["deci"], - data["addr"]) + data[DATA_VOUCHER_SYMBOLS], + data[DATA_VOUCHER_BALANCES], + data[DATA_VOUCHER_DECIMALS], + data[DATA_VOUCHER_ADDRESSES], + ) if symbol == "" { return nil, nil @@ -75,9 +101,10 @@ func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, 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) { @@ -97,51 +124,37 @@ func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, // StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore. func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { - entries := map[DataTyp][]byte{ - DATA_TEMPORARY_SYM: []byte(data.TokenSymbol), - DATA_TEMPORARY_BAL: []byte(data.Balance), - DATA_TEMPORARY_DECIMAL: []byte(data.TokenDecimals), - DATA_TEMPORARY_ADDRESS: []byte(data.ContractAddress), + 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 } - for key, value := range entries { - if err := store.WriteEntry(ctx, sessionId, key, value); 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) { - keys := []DataTyp{ - DATA_TEMPORARY_SYM, - DATA_TEMPORARY_BAL, - DATA_TEMPORARY_DECIMAL, - DATA_TEMPORARY_ADDRESS, + 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{} - values := make([][]byte, len(keys)) - for i, key := range keys { - value, err := store.ReadEntry(ctx, sessionId, key) - if err != nil { - return nil, err - } - values[i] = value - } - - data.TokenSymbol = string(values[0]) - data.Balance = string(values[1]) - data.TokenDecimals = string(values[2]) - data.ContractAddress = string(values[3]) + 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. +// UpdateVoucherData updates 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), diff --git a/internal/utils/vouchers_test.go b/common/vouchers_test.go similarity index 84% rename from internal/utils/vouchers_test.go rename to common/vouchers_test.go index a609d27..ba6cd60 100644 --- a/internal/utils/vouchers_test.go +++ b/common/vouchers_test.go @@ -1,14 +1,16 @@ -package utils +package common import ( "context" + "fmt" "testing" - "git.grassecon.net/urdt/ussd/internal/storage" "github.com/alecthomas/assert/v2" "github.com/stretchr/testify/require" + visedb "git.defalsify.org/vise.git/db" memdb "git.defalsify.org/vise.git/db/mem" + "git.grassecon.net/urdt/ussd/internal/storage" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" ) @@ -58,13 +60,13 @@ func TestMatchVoucher(t *testing.T) { func TestProcessVouchers(t *testing.T) { holdings := []dataserviceapi.TokenHoldings{ - {ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"}, - {ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"}, + {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:200", + Balances: "1:100\n2:20000", Decimals: "1:6\n2:4", Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa", } @@ -82,19 +84,21 @@ func TestGetVoucherData(t *testing.T) { if err != nil { t.Fatal(err) } - spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) + + prefix := ToBytes(visedb.DATATYPE_USERDATA) + spdb := storage.NewSubPrefixDb(db, prefix) // 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"), + mockData := map[DataTyp][]byte{ + DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"), + DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"), + DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"), + DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), } // Put the data for key, value := range mockData { - err = spdb.Put(ctx, []byte(key), []byte(value)) + err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value)) if err != nil { t.Fatal(err) } @@ -126,18 +130,11 @@ func TestStoreTemporaryVoucher(t *testing.T) { require.NoError(t, err) // Verify stored data - expectedEntries := map[DataTyp][]byte{ - DATA_TEMPORARY_SYM: []byte("SRF"), - DATA_TEMPORARY_BAL: []byte("200"), - DATA_TEMPORARY_DECIMAL: []byte("6"), - DATA_TEMPORARY_ADDRESS: []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"), - } + expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9") - for key, expectedValue := range expectedEntries { - storedValue, err := store.ReadEntry(ctx, sessionId, key) - require.NoError(t, err) - require.Equal(t, expectedValue, storedValue, "Mismatch for key %v", key) - } + 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) { diff --git a/config/config.go b/config/config.go index 43466c3..3a8e8ed 100644 --- a/config/config.go +++ b/config/config.go @@ -1,18 +1,74 @@ package config -import "git.grassecon.net/urdt/ussd/initializers" +import ( + "net/url" -var ( - CreateAccountURL string - TrackStatusURL string - BalanceURL string - TrackURL 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", "http://localhost:5003/api/v2/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/") - TrackURL = initializers.GetEnv("TRACK_URL", "http://localhost:5003/api/v2/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" + AliasPrefix = "api/v1/alias" +) + +var ( + custodialURLBase string + dataURLBase string + BearerToken string +) + +var ( + CreateAccountURL string + TrackStatusURL string + BalanceURL string + TrackURL string + TokenTransferURL string + VoucherHoldingsURL string + VoucherTransfersURL string + VoucherDataURL string + CheckAliasURL 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) + CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) + + return nil } diff --git a/debug/cap.go b/debug/cap.go new file mode 100644 index 0000000..458bb48 --- /dev/null +++ b/debug/cap.go @@ -0,0 +1,5 @@ +package debug + +var ( + DebugCap uint32 +) diff --git a/debug/db.go b/debug/db.go new file mode 100644 index 0000000..ec9e58f --- /dev/null +++ b/debug/db.go @@ -0,0 +1,84 @@ +package debug + +import ( + "fmt" + "encoding/binary" + + "git.grassecon.net/urdt/ussd/common" + "git.defalsify.org/vise.git/db" +) + +var ( + dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string) +) + +type KeyInfo struct { + SessionId string + Typ uint8 + SubTyp common.DataTyp + Label string + Description string +} + +func (k KeyInfo) String() string { + v := uint16(k.SubTyp) + s := subTypToString(k.SubTyp) + if s == "" { + v = uint16(k.Typ) + s = typToString(k.Typ) + } + return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v) +} + +func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) { + o := KeyInfo{} + b := []byte(sessionId) + + if len(k) <= len(b) { + return o, fmt.Errorf("storage key missing") + } + + o.SessionId = sessionId + + o.Typ = uint8(k[0]) + k = k[1:] + o.SessionId = string(k[:len(b)]) + k = k[len(b):] + + if o.Typ == db.DATATYPE_USERDATA { + if len(k) == 0 { + return o, fmt.Errorf("missing subtype key") + } + v := binary.BigEndian.Uint16(k[:2]) + o.SubTyp = common.DataTyp(v) + o.Label = subTypToString(o.SubTyp) + k = k[2:] + } else { + o.Label = typToString(o.Typ) + } + + if len(k) != 0 { + return o, fmt.Errorf("excess key information") + } + + return o, nil +} + +func FromKey(k []byte) (KeyInfo, error) { + o := KeyInfo{} + + if len(k) < 4 { + return o, fmt.Errorf("insufficient key length") + } + + sessionIdBytes := k[1:len(k)-2] + return ToKeyInfo(k, string(sessionIdBytes)) +} + +func subTypToString(v common.DataTyp) string { + return dbTypStr[v + db.DATATYPE_USERDATA + 1] +} + +func typToString(v uint8) string { + return dbTypStr[common.DataTyp(uint16(v))] +} diff --git a/debug/db_debug.go b/debug/db_debug.go new file mode 100644 index 0000000..ed2dd66 --- /dev/null +++ b/debug/db_debug.go @@ -0,0 +1,48 @@ +// +build debugdb + +package debug + +import ( + "git.defalsify.org/vise.git/db" + + "git.grassecon.net/urdt/ussd/common" +) + +func init() { + DebugCap |= 1 + dbTypStr[db.DATATYPE_STATE] = "internal state" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT] = "account" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_CREATED] = "account created" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_CUSTODIAL_ID] = "custodial id" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_STATUS] = "account status" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols" + dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals" +} diff --git a/debug/db_test.go b/debug/db_test.go new file mode 100644 index 0000000..25f781d --- /dev/null +++ b/debug/db_test.go @@ -0,0 +1,78 @@ +package debug + +import ( + "testing" + + "git.grassecon.net/urdt/ussd/common" + "git.defalsify.org/vise.git/db" +) + +func TestDebugDbSubKeyInfo(t *testing.T) { + s := "foo" + b := []byte{0x20} + b = append(b, []byte(s)...) + b = append(b, []byte{0x00, 0x02}...) + r, err := ToKeyInfo(b, s) + if err != nil { + t.Fatal(err) + } + if r.SessionId != s { + t.Fatalf("expected %s, got %s", s, r.SessionId) + } + if r.Typ != 32 { + t.Fatalf("expected 64, got %d", r.Typ) + } + if r.SubTyp != 2 { + t.Fatalf("expected 2, got %d", r.SubTyp) + } + if DebugCap & 1 > 0 { + if r.Label != "tracking id" { + t.Fatalf("expected 'tracking id', got '%s'", r.Label) + } + } +} + +func TestDebugDbKeyInfo(t *testing.T) { + s := "bar" + b := []byte{0x10} + b = append(b, []byte(s)...) + r, err := ToKeyInfo(b, s) + if err != nil { + t.Fatal(err) + } + if r.SessionId != s { + t.Fatalf("expected %s, got %s", s, r.SessionId) + } + if r.Typ != 16 { + t.Fatalf("expected 16, got %d", r.Typ) + } + if DebugCap & 1 > 0 { + if r.Label != "internal state" { + t.Fatalf("expected 'internal_state', got '%s'", r.Label) + } + } +} + +func TestDebugDbKeyInfoRestore(t *testing.T) { + s := "bar" + b := []byte{db.DATATYPE_USERDATA} + b = append(b, []byte(s)...) + k := common.ToBytes(common.DATA_ACTIVE_SYM) + b = append(b, k...) + + r, err := ToKeyInfo(b, s) + if err != nil { + t.Fatal(err) + } + if r.SessionId != s { + t.Fatalf("expected %s, got %s", s, r.SessionId) + } + if r.Typ != 32 { + t.Fatalf("expected 32, got %d", r.Typ) + } + if DebugCap & 1 > 0 { + if r.Label != "active sym" { + t.Fatalf("expected 'active sym', got '%s'", r.Label) + } + } +} diff --git a/dev/dialoguss/sample_user.yaml b/dev/dialoguss/sample_user.yaml new file mode 100644 index 0000000..83e690e --- /dev/null +++ b/dev/dialoguss/sample_user.yaml @@ -0,0 +1,3 @@ +url: http://localhost:7123 +dial: "*384*96#" +phoneNumber: +254722123456 diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml new file mode 100644 index 0000000..5d65b54 --- /dev/null +++ b/dev/docker-compose.yaml @@ -0,0 +1,21 @@ +services: + ussd-pg-store: + image: postgres:17-alpine + restart: unless-stopped + user: postgres + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_USER=postgres + volumes: + - ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql + - ussd-pg:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 +volumes: + ussd-pg: + driver: local diff --git a/dev/init_db.sql b/dev/init_db.sql new file mode 100644 index 0000000..9ee26b8 --- /dev/null +++ b/dev/init_db.sql @@ -0,0 +1 @@ +CREATE DATABASE urdt_ussd; \ No newline at end of file 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/devtools/gen/main.go b/devtools/gen/main.go new file mode 100644 index 0000000..b9e2aed --- /dev/null +++ b/devtools/gen/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "crypto/sha1" + "flag" + "fmt" + "os" + "path" + + "git.defalsify.org/vise.git/logging" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/initializers" + "git.grassecon.net/urdt/ussd/common" +) + +var ( + logg = logging.NewVanilla() + scriptDir = path.Join("services", "registration") +) + +func init() { + initializers.LoadEnvVariables() +} + + +func main() { + config.LoadConfig() + + var dbDir string + var sessionId string + var database string + var engineDebug bool + + flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") + flag.StringVar(&database, "db", "gdbm", "database to be used") + flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.BoolVar(&engineDebug, "d", false, "use engine debug output") + flag.Parse() + + ctx := context.Background() + ctx = context.WithValue(ctx, "SessionId", sessionId) + ctx = context.WithValue(ctx, "Database", database) + + resourceDir := scriptDir + menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + + store, err := menuStorageService.GetUserdataDb(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + userStore := common.UserDataStore{store} + + h := sha1.New() + h.Write([]byte(sessionId)) + address := h.Sum(nil) + addressString := fmt.Sprintf("%x", address) + + err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString)) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + err = store.Close() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + +} diff --git a/devtools/store/main.go b/devtools/store/main.go new file mode 100644 index 0000000..9262703 --- /dev/null +++ b/devtools/store/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "path" + + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" + "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/debug" + "git.defalsify.org/vise.git/logging" +) + +var ( + logg = logging.NewVanilla() + scriptDir = path.Join("services", "registration") +) + +func init() { + initializers.LoadEnvVariables() +} + + +func main() { + config.LoadConfig() + + var dbDir string + var sessionId string + var database string + var engineDebug bool + + flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") + flag.StringVar(&database, "db", "gdbm", "database to be used") + flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.BoolVar(&engineDebug, "d", false, "use engine debug output") + flag.Parse() + + ctx := context.Background() + ctx = context.WithValue(ctx, "SessionId", sessionId) + ctx = context.WithValue(ctx, "Database", database) + + resourceDir := scriptDir + menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + + store, err := menuStorageService.GetUserdataDb(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + d, err := store.Dump(ctx, []byte(sessionId)) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + for true { + k, v := d.Next(ctx) + if k == nil { + break + } + o, err := debug.FromKey(k) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + fmt.Printf("%vValue: %v\n\n", o, v) + } + + err = store.Close() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/doc/data.md b/doc/data.md new file mode 100644 index 0000000..7a41f0c --- /dev/null +++ b/doc/data.md @@ -0,0 +1,28 @@ +# Internals + +## Version + +This document describes component versions: + +* `urdt-ussd` `v0.5.0-beta` +* `go-vise` `v0.2.2` + + +## User profile data + +All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix. + +For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key: + +``` +0x322b3235343132330002 + +prefix sessionid subprefix +32 2b323534313233 0002 +``` + +### Sub-prefixes + +All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_` + +Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item. diff --git a/go.mod b/go.mod index eef74b9..e1b7ddb 100644 --- a/go.mod +++ b/go.mod @@ -2,29 +2,16 @@ module git.grassecon.net/urdt/ussd go 1.23.0 -toolchain go1.23.2 - require ( - git.defalsify.org/vise.git v0.2.1-0.20241031204035-b588301738ed + git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 github.com/alecthomas/assert/v2 v2.2.2 + github.com/gofrs/uuid v4.4.0+incompatible github.com/grassrootseconomics/eth-custodial v1.3.0-beta - github.com/peteole/testdata-loader v0.3.0 - gopkg.in/leonelquinteros/gotext.v1 v1.3.1 -) - -require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a - -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/grassrootseconomics/ussd-data-service v1.2.0-beta 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 + github.com/peteole/testdata-loader v0.3.0 + github.com/stretchr/testify v1.9.0 + gopkg.in/leonelquinteros/gotext.v1 v1.3.1 ) require ( @@ -33,13 +20,20 @@ require ( github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // 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/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/kr/text v0.2.0 // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 github.com/x448/float16 v0.8.4 // 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 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c8fa4cd..ef7b782 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.defalsify.org/vise.git v0.2.1-0.20241031204035-b588301738ed h1:4TrsfbK7NKgsa7KjMPlnV/tjYTkAAXP5PWAZzUfzCdI= -git.defalsify.org/vise.git v0.2.1-0.20241031204035-b588301738ed/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= +git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 h1:GYUVXRUtMpA40T4COeAduoay6CIgXjD5cfDYZOTFIKw= +git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= @@ -18,8 +18,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 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/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= +github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= 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= diff --git a/internal/handlers/base.go b/internal/handlers/base.go index 4d2aa4c..755cca4 100644 --- a/internal/handlers/base.go +++ b/internal/handlers/base.go @@ -55,6 +55,9 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) } f.hn = f.hn.WithPersister(rqs.Storage.Persister) + defer func() { + f.hn.Exit() + }() eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) en, ok := eni.(*engine.DefaultEngine) if !ok { diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 311e694..a14cf59 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,8 +63,8 @@ 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 } @@ -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) @@ -92,12 +103,26 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn 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("check_transactions", ussdHandlers.CheckTransactions) + ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) + ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) + ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems) + ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack) return ussdHandlers, nil } diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go deleted file mode 100644 index 890d1db..0000000 --- a/internal/handlers/server/accountservice.go +++ /dev/null @@ -1,185 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - - "git.grassecon.net/urdt/ussd/config" - "git.grassecon.net/urdt/ussd/internal/models" - "github.com/grassrootseconomics/eth-custodial/pkg/api" -) - -var ( - okResponse api.OKResponse - errResponse api.ErrResponse -) - -type AccountServiceInterface interface { - CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) - CreateAccount(ctx context.Context) (*api.OKResponse, error) - CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) - TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) - FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, 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) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) { - resp, err := http.Get(config.BalanceURL + 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 - -} - -func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) { - var err error - // Construct the URL with the path parameter - url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-GE-KEY", "xd") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - errResponse.Description = err.Error() - return nil, err - } - if resp.StatusCode >= http.StatusBadRequest { - err := json.Unmarshal([]byte(body), &errResponse) - if err != nil { - return nil, err - } - return nil, errors.New(errResponse.Description) - } - err = json.Unmarshal([]byte(body), &okResponse) - if err != nil { - return nil, err - } - if len(okResponse.Result) == 0 { - return nil, errors.New("Empty api result") - } - return &okResponse, nil - -} - -// 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.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(ctx context.Context) (*api.OKResponse, error) { - var err error - - // Create a new request - req, err := http.NewRequest("POST", config.CreateAccountURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-GE-KEY", "xd") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - errResponse.Description = err.Error() - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - if resp.StatusCode >= http.StatusBadRequest { - err := json.Unmarshal([]byte(body), &errResponse) - if err != nil { - return nil, err - } - return nil, errors.New(errResponse.Description) - } - err = json.Unmarshal([]byte(body), &okResponse) - if err != nil { - return nil, err - } - if len(okResponse.Result) == 0 { - return nil, errors.New("Empty api result") - } - return &okResponse, nil -} - -// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint -// Parameters: -// - publicKey: The public key associated with the account. -func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) { - file, err := os.Open("sample_tokens.json") - if err != nil { - return nil, err - } - defer file.Close() - var holdings models.VoucherHoldingResponse - - if err := json.NewDecoder(file).Decode(&holdings); err != nil { - return nil, err - } - return &holdings, nil -} diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 7f3068f..0b8ea64 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -10,7 +10,6 @@ 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" @@ -20,21 +19,32 @@ import ( "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" "git.grassecon.net/urdt/ussd/common" - "git.grassecon.net/urdt/ussd/internal/handlers/server" "git.grassecon.net/urdt/ussd/internal/utils" + "git.grassecon.net/urdt/ussd/models" + "git.grassecon.net/urdt/ussd/remote" "gopkg.in/leonelquinteros/gotext.v1" "git.grassecon.net/urdt/ussd/internal/storage" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" ) 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 ( + 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 +} + // FlagManager handles centralized flag management type FlagManager struct { parser *asm.FlagParser @@ -62,40 +72,37 @@ 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 + profile *models.Profile } -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")) + + // Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix + prefix := common.ToBytes(db.DATATYPE_USERDATA) + prefixDb := storage.NewSubPrefixDb(userdataStore, prefix) h := &Handlers{ userdataStore: userDb, flagManager: appFlags, + adminstore: adminstore, accountService: accountService, prefixDb: prefixDb, + profile: &models.Profile{Max: 6}, } 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 (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { if h.pe != nil { panic("persister already set") @@ -106,24 +113,42 @@ 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 } + defer func() { + h.Exit() + }() + 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") } - h.pe = nil logg.DebugCtxf(ctx, "handler has been initialized", "state", h.st, "cache", h.ca) return r, nil } +func (h *Handlers) Exit() { + h.pe = nil +} + // SetLanguage sets the language across the menu func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -132,13 +157,15 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r code := strings.Split(symbol, "_")[1] if !utils.IsValidISO639(code) { - return res, nil + //Fallback to english instead? + code = "eng" } res.FlagSet = append(res.FlagSet, state.FLAG_LANG) res.Content = code 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) @@ -148,16 +175,16 @@ 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 { flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") - okResponse, err := h.accountService.CreateAccount(ctx) + r, err := h.accountService.CreateAccount(ctx) if err != nil { return err } - trackingId := okResponse.Result["trackingId"].(string) - publicKey := okResponse.Result["publicKey"].(string) + trackingId := r.TrackingId + publicKey := r.PublicKey - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: trackingId, - utils.DATA_PUBLIC_KEY: publicKey, + data := map[common.DataTyp]string{ + common.DATA_TRACKING_ID: trackingId, + common.DATA_PUBLIC_KEY: publicKey, } store := h.userdataStore for key, value := range data { @@ -170,13 +197,12 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r if err != nil { return err } - err = store.WriteEntry(ctx, publicKeyNormalized, utils.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)) + 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 nil - } // CreateAccount checks if any account exists on the JSON data file, and if not @@ -190,16 +216,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_PUBLIC_KEY) 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 +} + +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") + } + store := h.userdataStore + 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 } @@ -221,7 +275,7 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) ( return res, nil } -// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_PIN +// 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) { @@ -234,7 +288,6 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt } flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") - accountPIN := string(input) // Validate that the PIN is a 4-digit number @@ -242,12 +295,36 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt res.FlagSet = append(res.FlagSet, flag_incorrect_pin) return res, nil } - res.FlagReset = append(res.FlagReset, flag_incorrect_pin) + store := h.userdataStore + err = store.WriteEntry(ctx, sessionId, 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 - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN)) + 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 } @@ -263,8 +340,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) { @@ -272,8 +350,9 @@ 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 @@ -294,11 +373,11 @@ func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte return res, fmt.Errorf("missing session") } 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(input, temporaryPin) { res.FlagSet = []uint32{flag_valid_pin} res.FlagReset = []uint32{flag_pin_mismatch} @@ -307,8 +386,9 @@ func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte res.FlagSet = []uint32{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 } @@ -336,17 +416,27 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte) firstName := string(input) store := h.userdataStore flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + firstNameSet := h.st.MatchFlag(flag_firstname_set, true) if allowUpdate { - temporaryFirstName, _ := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_FIRST_NAME) - err = store.WriteEntry(ctx, sessionId, utils.DATA_FIRST_NAME, []byte(temporaryFirstName)) + 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 } + res.FlagSet = append(res.FlagSet, flag_firstname_set) } else { - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_FIRST_NAME, []byte(firstName)) - if err != nil { - return res, err + if firstNameSet { + 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 + } + } else { + h.profile.InsertOrShift(0, firstName) } } @@ -361,24 +451,35 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte) if !ok { return res, fmt.Errorf("missing session") } + store := h.userdataStore familyName := string(input) flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_familyname_set, _ := h.flagManager.GetFlag("flag_familyname_set") allowUpdate := h.st.MatchFlag(flag_allow_update, true) + familyNameSet := h.st.MatchFlag(flag_familyname_set, true) if allowUpdate { - temporaryFamilyName, _ := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_FAMILY_NAME) - err = store.WriteEntry(ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(temporaryFamilyName)) + 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 } + res.FlagSet = append(res.FlagSet, flag_familyname_set) } else { - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_FAMILY_NAME, []byte(familyName)) - if err != nil { - return res, err + if familyNameSet { + 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 + } + } else { + h.profile.InsertOrShift(1, familyName) } } + return res, nil } @@ -393,18 +494,28 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou yob := string(input) store := h.userdataStore flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_yob_set, _ := h.flagManager.GetFlag("flag_yob_set") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + yobSet := h.st.MatchFlag(flag_yob_set, true) if allowUpdate { - temporaryYob, _ := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_YOB) - err = store.WriteEntry(ctx, sessionId, utils.DATA_YOB, []byte(temporaryYob)) + 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 } + res.FlagSet = append(res.FlagSet, flag_yob_set) } else { - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_YOB, []byte(yob)) - if err != nil { - return res, err + if yobSet { + 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 + } + } else { + h.profile.InsertOrShift(3, yob) } } @@ -423,18 +534,28 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) ( store := h.userdataStore flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_location_set, _ := h.flagManager.GetFlag("flag_location_set") allowUpdate := h.st.MatchFlag(flag_allow_update, true) + locationSet := h.st.MatchFlag(flag_location_set, true) if allowUpdate { - temporaryLocation, _ := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_LOCATION) - err = store.WriteEntry(ctx, sessionId, utils.DATA_LOCATION, []byte(temporaryLocation)) + 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 } + res.FlagSet = append(res.FlagSet, flag_location_set) } else { - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_LOCATION, []byte(location)) - if err != nil { - return res, err + if locationSet { + 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 + } + res.FlagSet = append(res.FlagSet, flag_location_set) + } else { + h.profile.InsertOrShift(4, location) } } @@ -453,18 +574,28 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re gender := strings.Split(symbol, "_")[1] store := h.userdataStore flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_gender_set, _ := h.flagManager.GetFlag("flag_gender_set") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + genderSet := h.st.MatchFlag(flag_gender_set, true) if allowUpdate { - temporaryGender, _ := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_GENDER) - err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(temporaryGender)) + 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 } + res.FlagSet = append(res.FlagSet, flag_gender_set) } else { - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_GENDER, []byte(gender)) - if err != nil { - return res, err + if genderSet { + 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 + } + } else { + h.profile.InsertOrShift(2, gender) } } @@ -479,22 +610,33 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) if !ok { return res, fmt.Errorf("missing session") } + offerings := string(input) store := h.userdataStore flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_offerings_set, _ := h.flagManager.GetFlag("flag_offerings_set") + allowUpdate := h.st.MatchFlag(flag_allow_update, true) + offeringsSet := h.st.MatchFlag(flag_offerings_set, true) if allowUpdate { - temporaryOfferings, _ := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_OFFERINGS) - err = store.WriteEntry(ctx, sessionId, utils.DATA_OFFERINGS, []byte(temporaryOfferings)) + temporaryOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) + err = store.WriteEntry(ctx, sessionId, common.DATA_OFFERINGS, []byte(temporaryOfferings)) if err != nil { + logg.ErrorCtxf(ctx, "failed to write offerings entry with", "key", common.DATA_TEMPORARY_VALUE, "value", offerings, "error", err) return res, err } + res.FlagSet = append(res.FlagSet, flag_offerings_set) } else { - err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_OFFERINGS, []byte(offerings)) - if err != nil { - return res, err + if offeringsSet { + 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 + } + } else { + h.profile.InsertOrShift(5, offerings) } } @@ -504,18 +646,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 } @@ -528,7 +675,7 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte 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) @@ -549,8 +696,9 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res 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 { @@ -581,6 +729,18 @@ func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []by return res, nil } +// Setback sets the flag_back_set flag when the navigation is back +func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + //TODO: + //Add check if the navigation is lateral nav instead of checking the input. + if string(input) == "0" { + flag_back_set, _ := h.flagManager.GetFlag("flag_back_set") + res.FlagSet = append(res.FlagSet, flag_back_set) + } + return res, nil +} + // CheckAccountStatus queries the API using the TrackingId and sets flags // based on the account status func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) { @@ -594,28 +754,31 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b if !ok { return res, fmt.Errorf("missing session") } + store := h.userdataStore - publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + 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 } - okResponse, err = h.accountService.TrackAccountStatus(ctx, string(publicKey)) + + r, err := h.accountService.TrackAccountStatus(ctx, string(publicKey)) if err != nil { 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) - isActive := okResponse.Result["active"].(bool) - if !ok { - return res, err - } - if isActive { + + 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 } @@ -655,7 +818,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 { @@ -664,12 +826,11 @@ func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (res return res, nil } - if len(date) == 4 { + if utils.IsValidYOb(date) { res.FlagReset = append(res.FlagReset, flag_incorrect_date_format) } else { res.FlagSet = append(res.FlagSet, flag_incorrect_date_format) } - return res, nil } @@ -678,7 +839,6 @@ 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 } @@ -701,7 +861,7 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) ( store := h.userdataStore // get the active sym and active balance - activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM) + activeSym, err := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) if err != nil { if db.IsNotFound(err) { balance := "0.00" @@ -709,84 +869,195 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) ( return res, nil } + logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", common.DATA_ACTIVE_SYM, "error", err) return res, err } - activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL) + 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 = l.Get("Balance: %s\n", fmt.Sprintf("%s %s", activeBal, activeSym)) + // Convert activeBal from []byte to float64 + balFloat, err := strconv.ParseFloat(string(activeBal), 64) + if err != nil { + logg.ErrorCtxf(ctx, "failed to parse activeBal as float", "value", string(activeBal), "error", err) + return res, err + } + + // Format to 2 decimal places + balStr := fmt.Sprintf("%.2f %s", balFloat, activeSym) + + res.Content = l.Get("Balance: %s\n", balStr) 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(ctx, 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 - } - 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 - } return res, nil } -// ValidateRecipient validates that the given input is a valid phone number. -func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { +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 !common.IsValidPhoneNumber(blockedNumber) { + res.FlagSet = append(res.FlagSet, flag_unregistered_number) + return res, nil + } + 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 +} + +// ValidateRecipient validates that the given input is valid. +func (h *Handlers) ValidateRecipient(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_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") + flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + recipient := string(input) - flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") - if recipient != "0" { - // mimic invalid number check - if recipient == "000" { + recipientType, err := common.CheckRecipient(recipient) + if err != nil { + // Invalid recipient format (not a phone number, address, or valid alias format) 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 { - return res, nil + logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", common.DATA_TEMPORARY_VALUE, "value", recipient, "error", err) + return res, err + } + + switch recipientType { + case "phone number": + // format the phone number + formattedNumber, err := common.FormatPhoneNumber(recipient) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err) + return res, err + } + + // Check if the phone number is registered + publicKey, err := store.ReadEntry(ctx, formattedNumber, common.DATA_PUBLIC_KEY) + if err != nil { + if db.IsNotFound(err) { + logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient) + 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 + } + + // Save the publicKey as the recipient + 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, err + } + + case "address": + // Save the valid Ethereum address as the recipient + err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recipient)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", recipient, "error", err) + return res, err + } + + case "alias": + // Call the API to validate and retrieve the address for the alias + r, aliasErr := h.accountService.CheckAliasAddress(ctx, recipient) + if aliasErr != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + res.Content = recipient + + logg.ErrorCtxf(ctx, "failed on CheckAliasAddress", "error", aliasErr) + return res, err + } + + // Alias validation succeeded, save the Ethereum address + err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(r.Address)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", r.Address, "error", err) + return res, err + } } } @@ -807,12 +1078,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 } @@ -822,6 +1093,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 @@ -834,7 +1130,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 } @@ -856,8 +1152,9 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res } store := h.userdataStore - activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL) + 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 } @@ -881,12 +1178,14 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) var balanceValue float64 // retrieve the active balance - activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL) + 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 } balanceValue, err = strconv.ParseFloat(string(activeBal), 64) if err != nil { + logg.ErrorCtxf(ctx, "Failed to convert the activeBal to a float", "error", err) return res, err } @@ -907,16 +1206,17 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) // Format the amount with 2 decimal places before saving formattedAmount := fmt.Sprintf("%.2f", inputAmount) - err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(formattedAmount)) + 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 = fmt.Sprintf("%s", formattedAmount) + 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 @@ -925,13 +1225,29 @@ 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 } +// 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 @@ -941,7 +1257,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res return res, fmt.Errorf("missing session") } - res.Content = string(sessionId) + res.Content = sessionId return res, nil } @@ -957,20 +1273,20 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res store := h.userdataStore // retrieve the active symbol - activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM) + 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, utils.DATA_AMOUNT) + 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 @@ -979,27 +1295,155 @@ 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 - amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT) - - recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT) - - activeSym, _ := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM) - - res.Content = l.Get("Your request has been sent. %s will receive %s %s from %s.", string(recipient), string(amount), string(activeSym), string(sessionId)) - - account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized") + 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 + + flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set") + flag_familyname_set, _ := h.flagManager.GetFlag("flag_familyname_set") + flag_yob_set, _ := h.flagManager.GetFlag("flag_yob_set") + flag_gender_set, _ := h.flagManager.GetFlag("flag_gender_set") + flag_location_set, _ := h.flagManager.GetFlag("flag_location_set") + flag_offerings_set, _ := h.flagManager.GetFlag("flag_offerings_set") + flag_back_set, _ := h.flagManager.GetFlag("flag_back_set") + + res.FlagReset = append(res.FlagReset, flag_back_set) + + 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.FlagSet = append(res.FlagSet, flag_firstname_set) + 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.FlagSet = append(res.FlagSet, flag_familyname_set) + 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.FlagSet = append(res.FlagSet, flag_gender_set) + 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.FlagSet = append(res.FlagSet, flag_yob_set) + 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.FlagSet = append(res.FlagSet, flag_location_set) + 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.FlagSet = append(res.FlagSet, flag_offerings_set) + res.Content = string(profileInfo) + default: + break + } + return res, nil } @@ -1030,22 +1474,15 @@ 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 - if familyName != defaultValue { - if firstName == defaultValue { - name = familyName - } else { - name = firstName + " " + familyName - } - } + name := utils.ConstructName(firstName, familyName, defaultValue) // Calculate age from year of birth age := defaultValue @@ -1092,46 +1529,68 @@ func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []by flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher") // check if the user has an active sym - _, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM) + _, err = store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) if err != nil { if db.IsNotFound(err) { - publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + 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, err + res.FlagSet = append(res.FlagSet, flag_no_active_voucher) + return res, nil } // Return if there is no voucher - if len(vouchersResp.Result.Holdings) == 0 { + if len(vouchersResp) == 0 { res.FlagSet = append(res.FlagSet, flag_no_active_voucher) return res, nil } // Use only the first voucher - firstVoucher := vouchersResp.Result.Holdings[0] + 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, utils.DATA_ACTIVE_SYM, []byte(defaultSym)) + 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, utils.DATA_ACTIVE_BAL, []byte(defaultBal)) + 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 } @@ -1150,9 +1609,10 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) } store := h.userdataStore - publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) if err != nil { - return res, 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 @@ -1161,18 +1621,50 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) return res, nil } - data := utils.ProcessVouchers(vouchersResp.Result.Holdings) + // check the current active sym and update the data + activeSym, _ := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM) + if activeSym != nil { + activeSymStr := string(activeSym) + + // Find the matching voucher data + var activeData *dataserviceapi.TokenHoldings + for _, voucher := range vouchersResp { + if voucher.TokenSymbol == activeSymStr { + activeData = &voucher + break + } + } + + if activeData == nil { + logg.ErrorCtxf(ctx, "activeSym not found in vouchers", "activeSym", activeSymStr) + return res, fmt.Errorf("activeSym %s not found in vouchers", activeSymStr) + } + + // Scale down the balance + scaledBalance := common.ScaleDownBalance(activeData.Balance, activeData.TokenDecimals) + + // Update the balance field with the scaled value + activeData.Balance = scaledBalance + + // Pass the matching voucher data to UpdateVoucherData + if err := common.UpdateVoucherData(ctx, h.userdataStore, sessionId, activeData); err != nil { + logg.ErrorCtxf(ctx, "failed on UpdateVoucherData", "error", err) + return res, err + } + } + + data := common.ProcessVouchers(vouchersResp) // Store all voucher data - dataMap := map[string]string{ - "sym": data.Symbols, - "bal": data.Balances, - "deci": data.Decimals, - "addr": data.Addresses, + dataMap := map[common.DataTyp]string{ + common.DATA_VOUCHER_SYMBOLS: data.Symbols, + common.DATA_VOUCHER_BALANCES: data.Balances, + common.DATA_VOUCHER_DECIMALS: data.Decimals, + common.DATA_VOUCHER_ADDRESSES: data.Addresses, } for key, value := range dataMap { - if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != nil { + if err := h.prefixDb.Put(ctx, []byte(common.ToBytes(key)), []byte(value)); err != nil { return res, nil } } @@ -1185,8 +1677,9 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) var res resource.Result // Read vouchers from the store - voucherData, err := h.prefixDb.Get(ctx, []byte("sym")) + voucherData, err := h.prefixDb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS)) if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the voucherData from prefixDb", "error", err) return res, err } @@ -1196,6 +1689,7 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) } // 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) @@ -1203,6 +1697,10 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r return res, fmt.Errorf("missing session") } + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") inputStr := string(input) @@ -1211,7 +1709,7 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r return res, nil } - metadata, err := utils.GetVoucherData(ctx, h.prefixDb, inputStr) + metadata, err := common.GetVoucherData(ctx, h.prefixDb, inputStr) if err != nil { return res, fmt.Errorf("failed to retrieve voucher data: %v", err) } @@ -1221,12 +1719,13 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r return res, nil } - if err := utils.StoreTemporaryVoucher(ctx, h.userdataStore, sessionId, metadata); err != 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) + res.Content = l.Get("Symbol: %s\nBalance: %s", metadata.TokenSymbol, metadata.Balance) return res, nil } @@ -1241,16 +1740,269 @@ func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (re } // Get temporary data - tempData, err := utils.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId) + 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 := utils.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil { + 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 + } + + res.Content = fmt.Sprintf( + "Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", voucherData.TokenName, voucherData.TokenSymbol, voucherData.TokenCommodity, voucherData.TokenLocation, + ) + + return res, nil +} + +// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb +func (h *Handlers) CheckTransactions(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_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + + 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 transactions from the API using the public key + transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + + // Return if there are no transactions + if len(transactionsResp) == 0 { + res.FlagSet = append(res.FlagSet, flag_no_transfers) + return res, nil + } + + data := common.ProcessTransfers(transactionsResp) + + // Store all transaction data + dataMap := map[common.DataTyp]string{ + common.DATA_TX_SENDERS: data.Senders, + common.DATA_TX_RECIPIENTS: data.Recipients, + common.DATA_TX_VALUES: data.TransferValues, + common.DATA_TX_ADDRESSES: data.Addresses, + common.DATA_TX_HASHES: data.TxHashes, + common.DATA_TX_DATES: data.Dates, + common.DATA_TX_SYMBOLS: data.Symbols, + common.DATA_TX_DECIMALS: data.Decimals, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(common.ToBytes(key)), []byte(value)); err != nil { + logg.ErrorCtxf(ctx, "failed to write to prefixDb", "error", err) + return res, err + } + } + + res.FlagReset = append(res.FlagReset, flag_no_transfers) + + return res, nil +} + +// GetTransactionsList fetches the list of transactions and formats them +func (h *Handlers) GetTransactionsList(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 + } + + // Read transactions from the store and format them + TransactionSenders, err := h.prefixDb.Get(ctx, common.ToBytes(common.DATA_TX_SENDERS)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionSenders from prefixDb", "error", err) + return res, err + } + TransactionSyms, err := h.prefixDb.Get(ctx, common.ToBytes(common.DATA_TX_SYMBOLS)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionSyms from prefixDb", "error", err) + return res, err + } + TransactionValues, err := h.prefixDb.Get(ctx, common.ToBytes(common.DATA_TX_VALUES)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionValues from prefixDb", "error", err) + return res, err + } + TransactionDates, err := h.prefixDb.Get(ctx, common.ToBytes(common.DATA_TX_DATES)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionDates from prefixDb", "error", err) + return res, err + } + + // Parse the data + senders := strings.Split(string(TransactionSenders), "\n") + syms := strings.Split(string(TransactionSyms), "\n") + values := strings.Split(string(TransactionValues), "\n") + dates := strings.Split(string(TransactionDates), "\n") + + var formattedTransactions []string + for i := 0; i < len(senders); i++ { + sender := strings.TrimSpace(senders[i]) + sym := strings.TrimSpace(syms[i]) + value := strings.TrimSpace(values[i]) + date := strings.Split(strings.TrimSpace(dates[i]), " ")[0] + + status := "received" + if sender == string(publicKey) { + status = "sent" + } + + formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date)) + } + + res.Content = strings.Join(formattedTransactions, "\n") + + return res, nil +} + +// ViewTransactionStatement retrieves the transaction statement +// and displays it to the user +func (h *Handlers) ViewTransactionStatement(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 + } + + flag_incorrect_statement, _ := h.flagManager.GetFlag("flag_incorrect_statement") + + inputStr := string(input) + if inputStr == "0" || inputStr == "99" || inputStr == "11" || inputStr == "22" { + res.FlagReset = append(res.FlagReset, flag_incorrect_statement) + return res, nil + } + + // Convert input string to integer + index, err := strconv.Atoi(strings.TrimSpace(inputStr)) + if err != nil { + return res, fmt.Errorf("invalid input: must be a number between 1 and 10") + } + + if index < 1 || index > 10 { + return res, fmt.Errorf("invalid input: index must be between 1 and 10") + } + + statement, err := common.GetTransferData(ctx, h.prefixDb, string(publicKey), index) + if err != nil { + return res, fmt.Errorf("failed to retrieve transfer data: %v", err) + } + + if statement == "" { + res.FlagSet = append(res.FlagSet, flag_incorrect_statement) + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_statement) + res.Content = statement + + return res, nil +} + +func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error { + var err error + store := h.userdataStore + profileFlagNames := []string{ + "flag_firstname_set", + "flag_familyname_set", + "flag_yob_set", + "flag_gender_set", + "flag_location_set", + "flag_offerings_set", + } + profileDataKeys := []common.DataTyp{ + common.DATA_FIRST_NAME, + common.DATA_FAMILY_NAME, + common.DATA_GENDER, + common.DATA_YOB, + common.DATA_LOCATION, + common.DATA_OFFERINGS, + } + for index, profileItem := range h.profile.ProfileItems { + // Ensure the profileItem is not "0"(is set) + if profileItem != "0" { + err = store.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err) + return err + } + + // Get the flag for the current index + flag, _ := h.flagManager.GetFlag(profileFlagNames[index]) + res.FlagSet = append(res.FlagSet, flag) + } + } + return nil +} + +// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags +func (h *Handlers) UpdateAllProfileItems(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") + } + err := h.insertProfileItems(ctx, sessionId, &res) + if err != nil { + return res, err + } + return res, nil +} diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 7c0689f..25470e8 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -2,30 +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/models" + "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" - "git.grassecon.net/urdt/ussd/internal/utils" "github.com/alecthomas/assert/v2" - "github.com/grassrootseconomics/eth-custodial/pkg/api" + testdataloader "github.com/peteole/testdata-loader" "github.com/stretchr/testify/require" + visedb "git.defalsify.org/vise.git/db" + memdb "git.defalsify.org/vise.git/db/mem" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" ) @@ -34,15 +32,47 @@ 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) + } + prefix := common.ToBytes(visedb.DATATYPE_USERDATA) + spdb := storage.NewSubPrefixDb(db, prefix) + + return spdb +} + func TestNewHandlers(t *testing.T) { + _, store := InitializeTestStore(t) + fm, err := NewFlagManager(flagsPath) accountService := testservice.TestAccountService{} if err != nil { t.Logf(err.Error()) } t.Run("Valid UserDataStore", func(t *testing.T) { - 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) } @@ -56,10 +86,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") } @@ -73,34 +100,30 @@ 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 + flag_account_created, err := fm.GetFlag("flag_account_created") if err != nil { t.Logf(err.Error()) } - // Define session ID and mock data - sessionId := "session123" - notFoundErr := db.ErrNotFound{} - ctx := context.WithValue(context.Background(), "SessionId", sessionId) tests := []struct { name string - serverResponse *api.OKResponse + serverResponse *models.AccountResult expectedResult resource.Result }{ { name: "Test account creation success", - serverResponse: &api.OKResponse{ - Ok: true, - Description: "Account creation successed", - Result: map[string]any{ - "trackingId": "1234567890", - "publicKey": "0xD3adB33f", - }, + serverResponse: &models.AccountResult{ + TrackingId: "1234567890", + PublicKey: "0xD3adB33f", }, expectedResult: resource.Result{ FlagSet: []uint32{flag_account_created}, @@ -109,47 +132,24 @@ func TestCreateAccount(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + mockAccountService := new(mocks.MockAccountService) - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - - // Create a Handlers instance with the mock data store h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, + userdataStore: store, + accountService: mockAccountService, flagManager: fm.parser, } - publicKey := tt.serverResponse.Result["publicKey"].(string) - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string), - utils.DATA_PUBLIC_KEY: publicKey, - } - - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACCOUNT_CREATED).Return([]byte(""), notFoundErr) - mockCreateAccountService.On("CreateAccount").Return(tt.serverResponse, nil) - - for key, value := range data { - mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil) - } - publicKeyNormalized, err := common.NormalizeHex(publicKey) - if err != nil { - t.Fatal(err) - } - - mockDataStore.On("WriteEntry", ctx, publicKeyNormalized, utils.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)).Return(nil) + mockAccountService.On("CreateAccount").Return(tt.serverResponse, nil) // Call the method you want to test - res, err := h.CreateAccount(ctx, "create_account", []byte("some-input")) + 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") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) }) } } @@ -174,23 +174,33 @@ func TestWithPersister_PanicWhenAlreadySet(t *testing.T) { } func TestSaveFirstname(t *testing.T) { - // Create new mocks - mockStore := new(mocks.MockUserDataStore) - mockState := state.NewState(16) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) - fm, err := NewFlagManager(flagsPath) + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + flag_firstname_set, _ := fm.GetFlag("flag_firstname_set") + + // Set the flag in the State + mockState := state.NewState(128) + mockState.SetFlag(flag_allow_update) + + expectedResult := resource.Result{} // 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_TEMPORARY_FIRST_NAME, []byte(firstName)).Return(nil) + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(firstName)); err != nil { + t.Fatal(err) + } + + expectedResult.FlagSet = []uint32{flag_firstname_set} // Create the Handlers instance with the mock store h := &Handlers{ - userdataStore: mockStore, + userdataStore: store, flagManager: fm.parser, st: mockState, } @@ -200,30 +210,41 @@ func TestSaveFirstname(t *testing.T) { // Assert results assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) + assert.Equal(t, expectedResult, 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) - mockState := state.NewState(16) + sessionId := "session123" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) - fm, err := NewFlagManager(flagsPath) + fm, _ := NewFlagManager(flagsPath) + + flag_allow_update, _ := fm.GetFlag("flag_allow_update") + flag_firstname_set, _ := fm.GetFlag("flag_familyname_set") + + // Set the flag in the State + mockState := state.NewState(128) + mockState.SetFlag(flag_allow_update) + + expectedResult := resource.Result{} + + expectedResult.FlagSet = []uint32{flag_firstname_set} // 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_TEMPORARY_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, } @@ -233,27 +254,235 @@ func TestSaveFamilyname(t *testing.T) { // Assert results assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) + assert.Equal(t, expectedResult, 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 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") + flag_yob_set, _ := fm.GetFlag("flag_yob_set") + + // Set the flag in the State + mockState := state.NewState(108) + mockState.SetFlag(flag_allow_update) + + expectedResult := resource.Result{} + + // Define test data + yob := "1980" + + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(yob)); err != nil { + t.Fatal(err) + } + + expectedResult.FlagSet = []uint32{flag_yob_set} + + // 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, expectedResult, 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") + flag_location_set, _ := fm.GetFlag("flag_location_set") + + // Set the flag in the State + mockState := state.NewState(108) + mockState.SetFlag(flag_allow_update) + + expectedResult := resource.Result{} + + // Define test data + location := "Kilifi" + + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(location)); err != nil { + t.Fatal(err) + } + + expectedResult.FlagSet = []uint32{flag_location_set} + + // 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, expectedResult, 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") + flag_offerings_set, _ := fm.GetFlag("flag_offerings_set") + + // Set the flag in the State + mockState := state.NewState(108) + mockState.SetFlag(flag_allow_update) + + expectedResult := resource.Result{} + + // Define test data + offerings := "Bananas" + + if err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(offerings)); err != nil { + t.Fatal(err) + } + + expectedResult.FlagSet = []uint32{flag_offerings_set} + + // 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, expectedResult, 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") + flag_gender_set, _ := fm.GetFlag("flag_gender_set") + + // Set the flag in the State + mockState := state.NewState(108) + 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, + } + + expectedResult := resource.Result{} + + // Call the method + res, err := h.SaveGender(ctx, "save_gender", tt.input) + + expectedResult.FlagSet = []uint32{flag_gender_set} + + // Assert results + assert.NoError(t, err) + assert.Equal(t, expectedResult, 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 { @@ -279,217 +508,34 @@ func TestSaveTemporaryPin(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_TEMPORARY_PIN, []byte(tt.input)).Return(nil) - // Call the method 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 new instances - mockStore := new(mocks.MockUserDataStore) - mockState := state.NewState(16) - - fm, err := NewFlagManager(flagsPath) - - // 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_TEMPORARY_YOB, []byte(yob)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - st: mockState, - flagManager: fm.parser, - } - - // 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) - mockState := state.NewState(16) - - fm, err := NewFlagManager(flagsPath) - - // 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_TEMPORARY_LOCATION, []byte(yob)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - st: mockState, - flagManager: fm.parser, - } - - // 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) - mockState := state.NewState(16) - - fm, err := NewFlagManager(flagsPath) - - // 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_TEMPORARY_OFFERINGS, []byte(offerings)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - st: mockState, - flagManager: fm.parser, - } - - // 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 mock instances - mockStore := new(mocks.MockUserDataStore) - mockState := state.NewState(16) - - fm, _ := NewFlagManager(flagsPath) - - // 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_TEMPORARY_GENDER - mockStore.On("WriteEntry", ctx, sessionId, expectedKey, []byte(tt.expectedGender)).Return(nil) - } else { - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_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, - flagManager: fm.parser, - } - - // 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_TEMPORARY_GENDER, []byte(tt.expectedGender)) - } else { - mockStore.AssertNotCalled(t, "WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_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, @@ -498,39 +544,33 @@ 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 TestGetSender(t *testing.T) { - mockStore := new(mocks.MockUserDataStore) + sessionId := "session123" + ctx, _ := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) - // Define test data - sessionId := "254712345678" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // 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, "get_sender", []byte("")) @@ -540,21 +580,27 @@ func TestGetSender(t *testing.T) { } func TestGetAmount(t *testing.T) { - mockDataStore := 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.03" activeSym := "SRF" - // Set up the expected behavior of the mock - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(activeSym), nil) - mockDataStore.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: mockDataStore, + userdataStore: store, } // Call the method @@ -564,31 +610,30 @@ func TestGetAmount(t *testing.T) { //Assert that the retrieved amount is what was set as the 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) { @@ -606,12 +651,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 @@ -650,25 +694,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 @@ -686,7 +729,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, @@ -694,25 +736,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 @@ -730,7 +771,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, @@ -738,25 +778,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 @@ -774,7 +813,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, @@ -782,25 +820,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 @@ -818,7 +855,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, @@ -826,43 +862,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, } @@ -897,14 +929,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)) @@ -914,33 +942,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, } @@ -975,7 +995,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)) @@ -984,37 +1003,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 TestVerifyCreatePin(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()) } - 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, } @@ -1041,16 +1054,12 @@ func TestVerifyCreatePin(t *testing.T) { }, } - typ := utils.DATA_TEMPORARY_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) - - // Set up the expected behavior of the mock - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(firstSetPin)).Return(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.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input)) @@ -1060,62 +1069,34 @@ func TestVerifyCreatePin(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 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 - serverResponse *api.OKResponse - response *models.TrackStatusResponse + publicKey []byte + response *models.TrackStatusResult expectedResult resource.Result }{ { - name: "Test when account is on the Sarafu network", - input: []byte("TrackingId1234"), - serverResponse: &api.OKResponse{ - Ok: true, - Description: "Account creation succeeded", - Result: map[string]any{ - "active": true, - }, - }, - 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}, @@ -1123,34 +1104,10 @@ func TestCheckAccountStatus(t *testing.T) { }, }, { - name: "Test when the account is not yet on the sarafu network", - 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", - }, - }, - }, - serverResponse: &api.OKResponse{ - Ok: true, - Description: "Account creation succeeded", - Result: map[string]any{ - "active": false, - }, + 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}, @@ -1160,59 +1117,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_PUBLIC_KEY).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) - mockCreateAccountService.On("TrackAccountStatus", string(tt.input)).Return(tt.serverResponse, 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 { @@ -1231,9 +1180,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) @@ -1242,40 +1188,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 }{ { @@ -1287,106 +1225,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 := "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 - Recipient []byte - Amount []byte - ActiveSym []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 initiate transaction", - Amount: []byte("0.002"), - ActiveSym: []byte("SRF"), - 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.002 SRF from 254712345678.", + 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_AMOUNT).Return(tt.Amount, nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return(tt.ActiveSym, 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 { @@ -1415,13 +1377,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 @@ -1477,21 +1436,22 @@ 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") - 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 { @@ -1531,11 +1491,10 @@ func TestValidateAmount(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Mock behavior for active balance retrieval - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL).Return(tt.activeBal, nil) - - // Mock behavior for storing the amount (if valid) - 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) @@ -1545,26 +1504,24 @@ func TestValidateAmount(t *testing.T) { // Assert the result matches the expected result assert.Equal(t, tt.expectedResult, res, "Expected result should match actual result") - - // Assert all expectations were met - mockDataStore.AssertExpectations(t) }) } } 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 @@ -1573,44 +1530,75 @@ func TestValidateRecipient(t *testing.T) { }{ { name: "Test with invalid recepient", - input: []byte("000"), + input: []byte("7?1234"), expectedResult: resource.Result{ FlagSet: []uint32{flag_invalid_recipient}, - Content: "000", + Content: "7?1234", }, }, { - 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{}, + }, + { + name: "Test with address", + input: []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"), + expectedResult: resource.Result{}, + }, + { + name: "Test with alias recepient", + input: []byte("alias123"), expectedResult: resource.Result{}, }, } + // store a public key for the valid recipient + err = store.WriteEntry(ctx, "+254711223344", 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 + mockAccountService := new(mocks.MockAccountService) + // Create the Handlers instance h := &Handlers{ - flagManager: fm.parser, - userdataStore: mockDataStore, + flagManager: fm.parser, + userdataStore: store, + accountService: mockAccountService, } + aliasResponse := &dataserviceapi.AliasAddress{ + Address: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + mockAccountService.On("CheckAliasAddress", string(tt.input)).Return(aliasResponse, nil) + // Call the method res, err := h.ValidateRecipient(ctx, "validate_recepient", 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 TestCheckBalance(t *testing.T) { + ctx, store := InitializeTestStore(t) + tests := []struct { name string sessionId string @@ -1622,29 +1610,33 @@ func TestCheckBalance(t *testing.T) { }{ { name: "User with active sym", - sessionId: "session456", + sessionId: "session123", publicKey: "0X98765432109", activeSym: "ETH", activeBal: "1.5", - expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"}, + expectedResult: resource.Result{Content: "Balance: 1.50 ETH\n"}, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockDataStore := new(mocks.MockUserDataStore) mockAccountService := new(mocks.MockAccountService) - ctx := context.WithValue(context.Background(), "SessionId", tt.sessionId) + ctx := context.WithValue(ctx, "SessionId", tt.sessionId) h := &Handlers{ - userdataStore: mockDataStore, + userdataStore: store, accountService: mockAccountService, } - // Mock for user with active sym - mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(tt.activeSym), nil) - mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_BAL).Return([]byte(tt.activeBal), 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) + } res, err := h.CheckBalance(ctx, "check_balance", []byte("")) @@ -1655,36 +1647,34 @@ func TestCheckBalance(t *testing.T) { assert.Equal(t, tt.expectedResult, res, "Result should match expected output") } - mockDataStore.AssertExpectations(t) 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{ @@ -1695,8 +1685,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{ @@ -1708,7 +1698,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{ @@ -1721,22 +1711,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") - }) } } @@ -1747,12 +1736,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) @@ -1775,44 +1762,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 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 @@ -1832,16 +1807,14 @@ 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") @@ -1849,89 +1822,55 @@ func TestConfirmPin(t *testing.T) { } } -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()) @@ -1940,140 +1879,129 @@ func TestSetDefaultVoucher(t *testing.T) { if err != nil { t.Logf(err.Error()) } - // Define session ID and mock data - sessionId := "session123" + publicKey := "0X13242618721" - notFoundErr := db.ErrNotFound{} - ctx := context.WithValue(context.Background(), "SessionId", sessionId) tests := []struct { name string - vouchersResp *models.VoucherHoldingResponse + vouchersResp []dataserviceapi.TokenHoldings expectedResult resource.Result }{ { - name: "Test set default voucher when no active voucher exists", - vouchersResp: &models.VoucherHoldingResponse{ - Ok: true, - Description: "Vouchers fetched successfully", - Result: models.VoucherResult{ - Holdings: []dataserviceapi.TokenHoldings{ - { - ContractAddress: "0x123", - TokenSymbol: "TOKEN1", - TokenDecimals: "18", - Balance: "100", - }, - }, - }, - }, - expectedResult: resource.Result{}, - }, - { - name: "Test no vouchers available", - vouchersResp: &models.VoucherHoldingResponse{ - Ok: true, - Description: "No vouchers available", - Result: models.VoucherResult{ - Holdings: []dataserviceapi.TokenHoldings{}, - }, - }, + 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) { - mockDataStore := new(mocks.MockUserDataStore) mockAccountService := new(mocks.MockAccountService) h := &Handlers{ - userdataStore: mockDataStore, + userdataStore: store, accountService: mockAccountService, flagManager: fm.parser, } - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(""), notFoundErr) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) + 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) - if len(tt.vouchersResp.Result.Holdings) > 0 { - firstVoucher := tt.vouchersResp.Result.Holdings[0] - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(firstVoucher.TokenSymbol)).Return(nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(firstVoucher.Balance)).Return(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") - mockDataStore.AssertExpectations(t) mockAccountService.AssertExpectations(t) }) } } func TestCheckVouchers(t *testing.T) { - mockDataStore := new(mocks.MockUserDataStore) mockAccountService := new(mocks.MockAccountService) - mockSubPrefixDb := new(mocks.MockSubPrefixDb) - sessionId := "session123" publicKey := "0X13242618721" + ctx, store := InitializeTestStore(t) + ctx = context.WithValue(ctx, "SessionId", sessionId) + spdb := InitializeTestSubPrefixDb(t, ctx) + h := &Handlers{ - userdataStore: mockDataStore, + userdataStore: store, accountService: mockAccountService, - prefixDb: mockSubPrefixDb, + prefixDb: spdb, } - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + err := store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey)) + if err != nil { + t.Fatal(err) + } - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) - - mockVouchersResponse := &models.VoucherHoldingResponse{} - mockVouchersResponse.Result.Holdings = []dataserviceapi.TokenHoldings{ + 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) - mockSubPrefixDb.On("Put", ctx, []byte("sym"), []byte("1:SRF\n2:MILO")).Return(nil) - mockSubPrefixDb.On("Put", ctx, []byte("bal"), []byte("1:100\n2:200")).Return(nil) - mockSubPrefixDb.On("Put", ctx, []byte("deci"), []byte("1:6\n2:4")).Return(nil) - mockSubPrefixDb.On("Put", ctx, []byte("addr"), []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa")).Return(nil) - - _, err := h.CheckVouchers(ctx, "check_vouchers", []byte("")) - + _, err = h.CheckVouchers(ctx, "check_vouchers", []byte("")) assert.NoError(t, err) - mockDataStore.AssertExpectations(t) + // Read voucher sym data from the store + voucherData, err := spdb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS)) + 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) { - mockSubPrefixDb := new(mocks.MockSubPrefixDb) - sessionId := "session123" ctx := context.WithValue(context.Background(), "SessionId", sessionId) + spdb := InitializeTestSubPrefixDb(t, ctx) + h := &Handlers{ - prefixDb: mockSubPrefixDb, + prefixDb: spdb, } - mockSubPrefixDb.On("Get", ctx, []byte("sym")).Return([]byte("1:SRF\n2:MILO"), nil) + expectedSym := []byte("1:SRF\n2:MILO") + + // Put voucher sym data from the store + err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym) + if err != nil { + t.Fatal(err) + } res, err := h.GetVoucherList(ctx, "", []byte("")) - assert.NoError(t, err) - assert.Contains(t, res.Content, "1:SRF\n2:MILO") - mockSubPrefixDb.AssertExpectations(t) + assert.NoError(t, err) + assert.Equal(t, res.Content, string(expectedSym)) } func TestViewVoucher(t *testing.T) { @@ -2081,55 +2009,49 @@ func TestViewVoucher(t *testing.T) { if err != nil { t.Logf(err.Error()) } - mockDataStore := new(mocks.MockUserDataStore) - mockSubPrefixDb := new(mocks.MockSubPrefixDb) - + ctx, store := InitializeTestStore(t) sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + + ctx = context.WithValue(ctx, "SessionId", sessionId) + + spdb := InitializeTestSubPrefixDb(t, ctx) h := &Handlers{ - userdataStore: mockDataStore, + userdataStore: store, flagManager: fm.parser, - prefixDb: mockSubPrefixDb, + prefixDb: spdb, } // Define mock voucher data - mockVoucherData := map[string]string{ - "sym": "1:SRF", - "bal": "1:100", - "deci": "1:6", - "addr": "1:0xd4c288865Ce", + mockData := map[common.DataTyp][]byte{ + common.DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"), + common.DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"), + common.DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"), + common.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), } - for key, value := range mockVoucherData { - mockSubPrefixDb.On("Get", ctx, []byte(key)).Return([]byte(value), nil) - } - - // Set up expectations for mockDataStore - expectedData := map[utils.DataTyp]string{ - utils.DATA_TEMPORARY_SYM: "SRF", - utils.DATA_TEMPORARY_BAL: "100", - utils.DATA_TEMPORARY_DECIMAL: "6", - utils.DATA_TEMPORARY_ADDRESS: "0xd4c288865Ce", - } - - for dataType, dataValue := range expectedData { - mockDataStore.On("WriteEntry", ctx, sessionId, dataType, []byte(dataValue)).Return(nil) + // Put the data + for key, value := range mockData { + err = spdb.Put(ctx, []byte(common.ToBytes(key)), []byte(value)) + if err != nil { + t.Fatal(err) + } } res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1")) assert.NoError(t, err) - assert.Contains(t, res.Content, "SRF\n100") - - mockDataStore.AssertExpectations(t) - mockSubPrefixDb.AssertExpectations(t) + assert.Equal(t, res.Content, "Symbol: SRF\nBalance: 100") } func TestSetVoucher(t *testing.T) { - mockDataStore := new(mocks.MockUserDataStore) - + ctx, store := InitializeTestStore(t) sessionId := "session123" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) + + ctx = context.WithValue(ctx, "SessionId", sessionId) + + h := &Handlers{ + userdataStore: store, + } // Define the temporary voucher data tempData := &dataserviceapi.TokenHoldings{ @@ -2139,47 +2061,55 @@ func TestSetVoucher(t *testing.T) { ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", } - // Define the expected active entries - activeEntries := map[utils.DataTyp][]byte{ - utils.DATA_ACTIVE_SYM: []byte(tempData.TokenSymbol), - utils.DATA_ACTIVE_BAL: []byte(tempData.Balance), - utils.DATA_ACTIVE_DECIMAL: []byte(tempData.TokenDecimals), - utils.DATA_ACTIVE_ADDRESS: []byte(tempData.ContractAddress), + 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) } - // Define the temporary entries to be cleared - tempEntries := map[utils.DataTyp][]byte{ - utils.DATA_TEMPORARY_SYM: []byte(""), - utils.DATA_TEMPORARY_BAL: []byte(""), - utils.DATA_TEMPORARY_DECIMAL: []byte(""), - utils.DATA_TEMPORARY_ADDRESS: []byte(""), - } - - // Mocking ReadEntry calls for temporary data retrieval - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM).Return([]byte(tempData.TokenSymbol), nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL).Return([]byte(tempData.Balance), nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_DECIMAL).Return([]byte(tempData.TokenDecimals), nil) - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_ADDRESS).Return([]byte(tempData.ContractAddress), nil) - - // Mocking WriteEntry calls for setting active data - for key, value := range activeEntries { - mockDataStore.On("WriteEntry", ctx, sessionId, key, value).Return(nil) - } - - // Mocking WriteEntry calls for clearing temporary data - for key, value := range tempEntries { - mockDataStore.On("WriteEntry", ctx, sessionId, key, value).Return(nil) - } - - h := &Handlers{ - userdataStore: mockDataStore, - } - - res, err := h.SetVoucher(ctx, "someSym", []byte{}) + res, err := h.SetVoucher(ctx, "set_voucher", []byte("")) assert.NoError(t, err) assert.Equal(t, string(tempData.TokenSymbol), res.Content) - - mockDataStore.AssertExpectations(t) +} + +func TestGetVoucherDetails(t *testing.T) { + ctx, store := InitializeTestStore(t) + fm, err := NewFlagManager(flagsPath) + if err != nil { + t.Logf(err.Error()) + } + mockAccountService := new(mocks.MockAccountService) + + sessionId := "session123" + ctx = context.WithValue(ctx, "SessionId", sessionId) + expectedResult := resource.Result{} + + tokA_AAddress := "0x0000000000000000000000000000000000000000" + + h := &Handlers{ + userdataStore: store, + flagManager: fm.parser, + accountService: mockAccountService, + } + err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tokA_AAddress)) + if err != nil { + t.Fatal(err) + } + tokenDetails := &models.VoucherDataResult{ + TokenName: "Token A", + TokenSymbol: "TOKA", + TokenLocation: "Kilifi,Kenya", + TokenCommodity: "Farming", + } + expectedResult.Content = fmt.Sprintf( + "Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation, + ) + mockAccountService.On("VoucherData", string(tokA_AAddress)).Return(tokenDetails, nil) + + res, err := h.GetVoucherDetails(ctx, "SessionId", []byte("")) + assert.NoError(t, err) + assert.Equal(t, expectedResult, res) } diff --git a/internal/models/accountresponse.go b/internal/models/accountresponse.go deleted file mode 100644 index efcc30b..0000000 --- a/internal/models/accountresponse.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -type AccountResponse struct { - Ok bool `json:"ok"` - Description string `json:"description"` // Include the description field - Result struct { - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - } `json:"result"` -} 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/tokenresponse.go b/internal/models/tokenresponse.go deleted file mode 100644 index d243d93..0000000 --- a/internal/models/tokenresponse.go +++ /dev/null @@ -1,18 +0,0 @@ -package models - -type ApiResponse struct { - OK bool `json:"ok"` - Description string `json:"description"` - Result Result `json:"result"` -} - -type Result struct { - Holdings []Holding `json:"holdings"` -} - -type Holding struct { - ContractAddress string `json:"contractAddress"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimals string `json:"tokenDecimals"` - Balance string `json:"balance"` -} diff --git a/internal/models/trackstatusresponse.go b/internal/models/trackstatusresponse.go deleted file mode 100644 index 6b96d90..0000000 --- a/internal/models/trackstatusresponse.go +++ /dev/null @@ -1,26 +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/models/vouchersresponse.go b/internal/models/vouchersresponse.go deleted file mode 100644 index 09b085d..0000000 --- a/internal/models/vouchersresponse.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" - -type VoucherHoldingResponse struct { - Ok bool `json:"ok"` - Description string `json:"description"` - Result VoucherResult `json:"result"` -} - -// VoucherResult holds the list of token holdings -type VoucherResult struct { - Holdings []dataserviceapi.TokenHoldings `json:"holdings"` -} diff --git a/internal/storage/gdbm.go b/internal/storage/gdbm.go index 0e53853..ede1a49 100644 --- a/internal/storage/gdbm.go +++ b/internal/storage/gdbm.go @@ -4,8 +4,8 @@ import ( "context" "git.defalsify.org/vise.git/db" - "git.defalsify.org/vise.git/lang" gdbmdb "git.defalsify.org/vise.git/db/gdbm" + "git.defalsify.org/vise.git/lang" ) var ( @@ -115,3 +115,9 @@ func(tdb *ThreadGdbmDb) Close() error { tdb.db = nil return err } + +func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) { + tdb.reserve() + defer tdb.release() + return tdb.db.Dump(ctx, key) +} diff --git a/internal/storage/storageservice.go b/internal/storage/storageservice.go index 9fa1839..ca28bbb 100644 --- a/internal/storage/storageservice.go +++ b/internal/storage/storageservice.go @@ -41,10 +41,13 @@ func buildConnStr() string { dbName := initializers.GetEnv("DB_NAME", "") port := initializers.GetEnv("DB_PORT", "5432") - return fmt.Sprintf( + connString := fmt.Sprintf( "postgres://%s:%s@%s:%s/%s", user, password, host, port, dbName, ) + logg.Debugf("pg conn string", "conn", connString) + + return connString } func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { diff --git a/internal/storage/db.go b/internal/storage/sub_prefix_db.go similarity index 92% rename from internal/storage/db.go rename to internal/storage/sub_prefix_db.go index a8e0b5f..bdcdada 100644 --- a/internal/storage/db.go +++ b/internal/storage/sub_prefix_db.go @@ -8,9 +8,6 @@ import ( const ( DATATYPE_USERSUB = 64 -) - -const ( SUBPREFIX_TIME = uint16(1) ) @@ -43,13 +40,13 @@ func(s *SubPrefixDb) toKey(k []byte) []byte { } func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { - s.store.SetPrefix(DATATYPE_USERSUB) + s.store.SetPrefix(db.DATATYPE_USERDATA) key = s.toKey(key) return s.store.Get(ctx, key) } func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { - s.store.SetPrefix(DATATYPE_USERSUB) + s.store.SetPrefix(db.DATATYPE_USERDATA) key = s.toKey(key) return s.store.Put(ctx, key, val) } diff --git a/internal/storage/db_test.go b/internal/storage/sub_prefix_db_test.go similarity index 100% rename from internal/storage/db_test.go rename to internal/storage/sub_prefix_db_test.go diff --git a/internal/testutil/TestEngine.go b/internal/testutil/TestEngine.go index bd6bc7f..3fcb307 100644 --- a/internal/testutil/TestEngine.go +++ b/internal/testutil/TestEngine.go @@ -11,11 +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 ( @@ -73,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) @@ -83,7 +83,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { } if testtag.AccountService == nil { - testtag.AccountService = &server.AccountService{} + testtag.AccountService = &remote.AccountService{} } switch testtag.AccountService.(type) { @@ -91,7 +91,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { go func() { eventChannel <- false }() - case *server.AccountService: + case *remote.AccountService: go func() { time.Sleep(5 * time.Second) // Wait for 5 seconds eventChannel <- true diff --git a/internal/testutil/mocks/dbmock.go b/internal/testutil/mocks/dbmock.go deleted file mode 100644 index 0b40eab..0000000 --- a/internal/testutil/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/testutil/mocks/servicemock.go b/internal/testutil/mocks/servicemock.go index de0e99a..59d7205 100644 --- a/internal/testutil/mocks/servicemock.go +++ b/internal/testutil/mocks/servicemock.go @@ -3,8 +3,8 @@ package mocks import ( "context" - "git.grassecon.net/urdt/ussd/internal/models" - "github.com/grassrootseconomics/eth-custodial/pkg/api" + "git.grassecon.net/urdt/ussd/models" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" "github.com/stretchr/testify/mock" ) @@ -13,28 +13,42 @@ type MockAccountService struct { mock.Mock } -func (m *MockAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) { +func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { args := m.Called() - return args.Get(0).(*api.OKResponse), args.Error(1) + return args.Get(0).(*models.AccountResult), args.Error(1) } -func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) { +func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { args := m.Called(publicKey) - return args.Get(0).(*models.BalanceResponse), args.Error(1) + return args.Get(0).(*models.BalanceResult), args.Error(1) } -func (m *MockAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) { +func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) { args := m.Called(trackingId) - return args.Get(0).(*models.TrackStatusResponse), args.Error(1) + return args.Get(0).(*models.TrackStatusResult), args.Error(1) } -func (m *MockAccountService) TrackAccountStatus(ctx context.Context,publicKey string) (*api.OKResponse, error) { +func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { args := m.Called(publicKey) - return args.Get(0).(*api.OKResponse), args.Error(1) + return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) } - -func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) { +func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { args := m.Called(publicKey) - return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1) + 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) +} + +func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { + args := m.Called(alias) + return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1) } diff --git a/internal/testutil/mocks/subprefixdbmock.go b/internal/testutil/mocks/subprefixdbmock.go deleted file mode 100644 index e98bcb6..0000000 --- a/internal/testutil/mocks/subprefixdbmock.go +++ /dev/null @@ -1,21 +0,0 @@ -package mocks - -import ( - "context" - - "github.com/stretchr/testify/mock" -) - -type MockSubPrefixDb struct { - mock.Mock -} - -func (m *MockSubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { - args := m.Called(ctx, key) - return args.Get(0).([]byte), args.Error(1) -} - -func (m *MockSubPrefixDb) Put(ctx context.Context, key, val []byte) error { - args := m.Called(ctx, key, val) - return args.Error(0) -} diff --git a/internal/testutil/mocks/userdbmock.go b/internal/testutil/mocks/userdbmock.go deleted file mode 100644 index ff3f18d..0000000 --- a/internal/testutil/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/testutil/testservice/TestAccountService.go b/internal/testutil/testservice/TestAccountService.go index 745b80d..96dacbc 100644 --- a/internal/testutil/testservice/TestAccountService.go +++ b/internal/testutil/testservice/TestAccountService.go @@ -3,88 +3,60 @@ package testservice import ( "context" "encoding/json" - "time" - "git.grassecon.net/urdt/ussd/internal/models" - "github.com/grassrootseconomics/eth-custodial/pkg/api" + "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) (*api.OKResponse, error) { - return &api.OKResponse{ - Ok: true, - Description: "Account creation succeeded", - Result: map[string]any{ - "trackingId": "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", - "publicKey": "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", - }, +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.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"), - }, +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) CheckAccountStatus(ctx context.Context, 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 +func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { + return &models.TrackStatusResult{ + Active: true, + }, nil } -func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) { - return &api.OKResponse{ - Ok: true, - Description: "Account creation succeeded", - Result: map[string]any{ - "active": true, +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) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) { - return &models.VoucherHoldingResponse{ - Ok: true, - Result: models.VoucherResult{ - Holdings: []dataserviceapi.TokenHoldings{ - { - ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", - TokenSymbol: "SRF", - TokenDecimals: "6", - Balance: "2745987", - }, - }, - }, +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 } + +func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { + return &dataserviceapi.AliasAddress{}, nil +} diff --git a/internal/testutil/testtag/offlinetest.go b/internal/testutil/testtag/offlinetest.go index 70e2e80..831bf09 100644 --- a/internal/testutil/testtag/offlinetest.go +++ b/internal/testutil/testtag/offlinetest.go @@ -3,10 +3,10 @@ package testtag import ( - "git.grassecon.net/urdt/ussd/internal/handlers/server" + "git.grassecon.net/urdt/ussd/remote" accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice" ) var ( - AccountService server.AccountServiceInterface = &accountservice.TestAccountService{} + AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{} ) diff --git a/internal/testutil/testtag/onlinetest.go b/internal/testutil/testtag/onlinetest.go index 92cbb14..835ba0d 100644 --- a/internal/testutil/testtag/onlinetest.go +++ b/internal/testutil/testtag/onlinetest.go @@ -2,8 +2,8 @@ package testtag -import "git.grassecon.net/urdt/ussd/internal/handlers/server" +import "git.grassecon.net/urdt/ussd/remote" var ( - AccountService server.AccountServiceInterface + AccountService remote.AccountServiceInterface ) diff --git a/internal/utils/adminstore.go b/internal/utils/adminstore.go new file mode 100644 index 0000000..a492479 --- /dev/null +++ b/internal/utils/adminstore.go @@ -0,0 +1,51 @@ +package utils + +import ( + "context" + + "git.defalsify.org/vise.git/db" + fsdb "git.defalsify.org/vise.git/db/fs" + "git.defalsify.org/vise.git/logging" +) + +var ( + logg = logging.NewVanilla().WithDomain("adminstore") +) + +type AdminStore struct { + ctx context.Context + FsStore db.Db +} + +func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) { + fsStore, err := getFsStore(ctx, fileName) + if err != nil { + return nil, err + } + return &AdminStore{ctx: ctx, FsStore: fsStore}, nil +} + +func getFsStore(ctx context.Context, connectStr string) (db.Db, error) { + fsStore := fsdb.NewFsDb() + err := fsStore.Connect(ctx, connectStr) + fsStore.SetPrefix(db.DATATYPE_USERDATA) + if err != nil { + return nil, err + } + return fsStore, nil +} + +// Checks if the given sessionId is listed as an admin. +func (as *AdminStore) IsAdmin(sessionId string) (bool, error) { + _, err := as.FsStore.Get(as.ctx, []byte(sessionId)) + if err != nil { + if db.IsNotFound(err) { + logg.Printf(logging.LVL_INFO, "Returning false because session id was not found") + return false, nil + } else { + return false, err + } + } + + return true, nil +} diff --git a/internal/utils/age.go b/internal/utils/age.go index 6b040e7..59056a8 100644 --- a/internal/utils/age.go +++ b/internal/utils/age.go @@ -1,6 +1,9 @@ package utils -import "time" +import ( + "strconv" + "time" +) // CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy // It adjusts for cases where the current date is before the birthday in the current year. @@ -25,11 +28,29 @@ func CalculateAge(birthdate, today time.Time) int { // It subtracts the YOB from the current year to determine the age. // // Parameters: -// yob: The year of birth as an integer. +// +// yob: The year of birth as an integer. // // Returns: -// The calculated age as an integer. +// +// The calculated age as an integer. func CalculateAgeWithYOB(yob int) int { - currentYear := time.Now().Year() - return currentYear - yob -} \ No newline at end of file + currentYear := time.Now().Year() + return currentYear - yob +} + + +//IsValidYob checks if the provided yob can be considered valid +func IsValidYOb(yob string) bool { + currentYear := time.Now().Year() + yearOfBirth, err := strconv.ParseInt(yob, 10, 64) + if err != nil { + return false + } + if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear { + return true + } else { + return false + } + +} diff --git a/internal/utils/db.go b/internal/utils/db.go deleted file mode 100644 index 61970ae..0000000 --- a/internal/utils/db.go +++ /dev/null @@ -1,54 +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_TEMPORARY_FIRST_NAME - DATA_FIRST_NAME - DATA_TEMPORARY_FAMILY_NAME - DATA_FAMILY_NAME - DATA_TEMPORARY_YOB - DATA_YOB - DATA_TEMPORARY_LOCATION - DATA_LOCATION - DATA_TEMPORARY_GENDER - DATA_GENDER - DATA_TEMPORARY_OFFERINGS - DATA_OFFERINGS - DATA_RECIPIENT - DATA_AMOUNT - DATA_TEMPORARY_PIN - DATA_VOUCHER_LIST - DATA_TEMPORARY_SYM - DATA_ACTIVE_SYM - DATA_TEMPORARY_BAL - DATA_ACTIVE_BAL - DATA_PUBLIC_KEY_REVERSE - DATA_TEMPORARY_DECIMAL - DATA_ACTIVE_DECIMAL - DATA_TEMPORARY_ADDRESS - DATA_ACTIVE_ADDRESS - -) - -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/internal/utils/name.go b/internal/utils/name.go new file mode 100644 index 0000000..ea403d5 --- /dev/null +++ b/internal/utils/name.go @@ -0,0 +1,17 @@ +package utils + +func ConstructName(firstName, familyName, defaultValue string) string { + name := defaultValue + if familyName != defaultValue { + if firstName != defaultValue { + name = firstName + " " + familyName + } else { + name = familyName + } + } else { + if firstName != defaultValue { + name = firstName + } + } + return name +} diff --git a/menutraversal_test/group_test.json b/menutraversal_test/group_test.json index 8b765f5..8f43ff5 100644 --- a/menutraversal_test/group_test.json +++ b/menutraversal_test/group_test.json @@ -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", @@ -54,7 +54,7 @@ }, { "input": "1235", - "expectedContent": "Incorrect pin\n1:retry\n9:Quit" + "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit" }, { "input": "1", @@ -62,7 +62,7 @@ }, { "input": "1234", - "expectedContent": "Select language:\n0:english\n1:kiswahili" + "expectedContent": "Select language:\n0:English\n1:Kiswahili" }, { "input": "0", @@ -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", @@ -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", @@ -167,7 +167,7 @@ ] }, { - "name": "menu_my_account_edit_firstname", + "name": "menu_my_account_edit_all_account_details_starting_from_firstname", "steps": [ { "input": "", @@ -187,6 +187,26 @@ }, { "input": "foo", + "expectedContent": "Enter family name:\n0:Back" + }, + { + "input": "bar", + "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1940", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", "expectedContent": "Please enter your PIN:" }, { @@ -197,10 +217,6 @@ "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" @@ -208,7 +224,7 @@ ] }, { - "name": "menu_my_account_edit_familyname", + "name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set", "steps": [ { "input": "", @@ -238,10 +254,6 @@ "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" @@ -250,7 +262,7 @@ ] }, { - "name": "menu_my_account_edit_gender", + "name": "menu_my_account_edit_gender_when_all_account__details_have_been_set", "steps": [ { "input": "", @@ -280,10 +292,6 @@ "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" @@ -291,7 +299,7 @@ ] }, { - "name": "menu_my_account_edit_yob", + "name": "menu_my_account_edit_yob_when_all_account__details_have_been_set", "steps": [ { "input": "", @@ -321,10 +329,6 @@ "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" @@ -332,7 +336,7 @@ ] }, { - "name": "menu_my_account_edit_location", + "name": "menu_my_account_edit_location_when_all_account_details_have_been_set", "steps": [ { "input": "", @@ -362,10 +366,6 @@ "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" @@ -373,7 +373,7 @@ ] }, { - "name": "menu_my_account_edit_offerings", + "name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set", "steps": [ { "input": "", @@ -403,10 +403,6 @@ "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" @@ -434,16 +430,12 @@ }, { "input": "1234", - "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back" + "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 84\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back" }, { "input": "0", "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" }, - { - "input": "0", - "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" - }, { "input": "0", "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" diff --git a/menutraversal_test/menu_traversal_test.go b/menutraversal_test/menu_traversal_test.go index d79b771..28d88db 100644 --- a/menutraversal_test/menu_traversal_test.go +++ b/menutraversal_test/menu_traversal_test.go @@ -3,6 +3,7 @@ package menutraversaltest import ( "bytes" "context" + "flag" "log" "math/rand" "os" @@ -15,14 +16,15 @@ import ( ) var ( - testData = driver.ReadData() - testStore = ".test_state" - groupTestFile = "group_test.json" - sessionID string - src = rand.NewSource(42) - g = rand.New(src) + testData = driver.ReadData() + testStore = ".test_state" + sessionID string + src = rand.NewSource(42) + g = rand.New(src) ) +var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests") + func GenerateSessionId() string { uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) v, err := uu.NewV4() @@ -337,7 +339,7 @@ func TestMainMenuSend(t *testing.T) { } func TestGroups(t *testing.T) { - groups, err := driver.LoadTestGroups(groupTestFile) + groups, err := driver.LoadTestGroups(*groupTestFile) if err != nil { log.Fatalf("Failed to load test groups: %v", err) } diff --git a/menutraversal_test/profile_edit_start_familyname.json b/menutraversal_test/profile_edit_start_familyname.json new file mode 100644 index 0000000..98325b0 --- /dev/null +++ b/menutraversal_test/profile_edit_start_familyname.json @@ -0,0 +1,68 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_all_account_details_starting_from_family_name", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "2", + "expectedContent": "Enter family name:\n0:Back" + }, + { + "input": "bar", + "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1940", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + + + + + + + + + + + \ No newline at end of file diff --git a/menutraversal_test/profile_edit_start_firstname.json b/menutraversal_test/profile_edit_start_firstname.json new file mode 100644 index 0000000..0f6be8b --- /dev/null +++ b/menutraversal_test/profile_edit_start_firstname.json @@ -0,0 +1,61 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_all_account_details_starting_from_firstname", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your first names:\n0:Back" + }, + { + "input": "foo", + "expectedContent": "Enter family name:\n0:Back" + }, + { + "input": "bar", + "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1940", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} diff --git a/menutraversal_test/profile_edit_start_gender.json b/menutraversal_test/profile_edit_start_gender.json new file mode 100644 index 0000000..afca12a --- /dev/null +++ b/menutraversal_test/profile_edit_start_gender.json @@ -0,0 +1,55 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_all_account_details_starting_from_gender", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "3", + "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1940", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + + \ No newline at end of file diff --git a/menutraversal_test/profile_edit_start_location.json b/menutraversal_test/profile_edit_start_location.json new file mode 100644 index 0000000..8852911 --- /dev/null +++ b/menutraversal_test/profile_edit_start_location.json @@ -0,0 +1,46 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_all_account_details_starting_from_location", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "5", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + \ No newline at end of file diff --git a/menutraversal_test/profile_edit_start_offerings.json b/menutraversal_test/profile_edit_start_offerings.json new file mode 100644 index 0000000..6aa40f6 --- /dev/null +++ b/menutraversal_test/profile_edit_start_offerings.json @@ -0,0 +1,42 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_all_account_details_starting_from_offerings", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "6", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + \ No newline at end of file diff --git a/menutraversal_test/profile_edit_start_yob.json b/menutraversal_test/profile_edit_start_yob.json new file mode 100644 index 0000000..45227f7 --- /dev/null +++ b/menutraversal_test/profile_edit_start_yob.json @@ -0,0 +1,50 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_all_account_details_starting_from_yob", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "4", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1940", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + \ No newline at end of file diff --git a/menutraversal_test/profile_edit_when_adjacent_item_set.json b/menutraversal_test/profile_edit_when_adjacent_item_set.json new file mode 100644 index 0000000..f8d7263 --- /dev/null +++ b/menutraversal_test/profile_edit_when_adjacent_item_set.json @@ -0,0 +1,70 @@ +{ + "groups": [ + { + "name": "menu_my_account_edit_familyname_when_adjacent_profile_information_set", + "steps": [ + { + "input": "", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + }, + { + "input": "3", + "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" + }, + { + "input": "1", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "3", + "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" + }, + { + "input": "1", + "expectedContent": "Enter your year of birth\n0:Back" + }, + { + "input": "1940", + "expectedContent": "Enter your location:\n0:Back" + }, + { + "input": "Kilifi", + "expectedContent": "Enter the services or goods you offer: \n0:Back" + }, + { + "input": "Bananas", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "2", + "expectedContent": "Enter family name:\n0:Back" + }, + { + "input": "foo2", + "expectedContent": "Please enter your PIN:" + }, + { + "input": "1234", + "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" + }, + { + "input": "0", + "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" + }, + { + "input": "0", + "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + } + ] + } + ] +} + \ No newline at end of file diff --git a/menutraversal_test/test_setup.json b/menutraversal_test/test_setup.json index 13166a4..c5860b4 100644 --- a/menutraversal_test/test_setup.json +++ b/menutraversal_test/test_setup.json @@ -7,11 +7,11 @@ "steps": [ { "input": "", - "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" + "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili" }, { "input": "0", - "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" + "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No" }, { "input": "0", @@ -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", @@ -40,11 +40,11 @@ "steps": [ { "input": "", - "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" + "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili" }, { "input": "0", - "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" + "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No" }, { "input": "1", @@ -53,7 +53,7 @@ ] }, { - "name": "send_with_invalid_inputs", + "name": "send_with_invite", "steps": [ { "input": "", @@ -61,43 +61,23 @@ }, { "input": "1", - "expectedContent": "Enter recipient's phone number:\n0:Back" + "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" }, { "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" + "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" }, { - "input": "065656", - "expectedContent": "{max_amount}\nEnter amount:\n0:Back" + "input": "0712345678", + "expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit" }, { - "input": "10000000", - "expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit" - }, - { - "input": "1", - "expectedContent": "{max_amount}\nEnter amount:\n0:Back" - }, - { - "input": "1.00", - "expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" - }, - { - "input": "1222", - "expectedContent": "Incorrect pin\n1:retry\n9:Quit" - }, - { - "input": "1", - "expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" - }, - { - "input": "1234", - "expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}." + "input": "2", + "expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later." } ] }, @@ -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/profile.go b/models/profile.go new file mode 100644 index 0000000..d698318 --- /dev/null +++ b/models/profile.go @@ -0,0 +1,18 @@ +package models + +type Profile struct { + ProfileItems []string + Max int +} + +func (p *Profile) InsertOrShift(index int, value string) { + if index < len(p.ProfileItems) { + p.ProfileItems = append(p.ProfileItems[:index], value) + } else { + for len(p.ProfileItems) < index { + p.ProfileItems = append(p.ProfileItems, "0") + } + p.ProfileItems = append(p.ProfileItems, "0") + p.ProfileItems[index] = value + } +} 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..9a10831 --- /dev/null +++ b/models/voucher_data_result.go @@ -0,0 +1,10 @@ +package models + +type VoucherDataResult struct { + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimals int `json:"tokenDecimals"` + SinkAddress string `json:"sinkAddress"` + TokenCommodity string `json:"tokenCommodity"` + TokenLocation string `json:"tokenLocation"` +} diff --git a/remote/accountservice.go b/remote/accountservice.go new file mode 100644 index 0000000..b0e9eb4 --- /dev/null +++ b/remote/accountservice.go @@ -0,0 +1,294 @@ +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) + CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, 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 +} + +// CheckAliasAddress retrieves the address of an alias from the API endpoint. +// Parameters: +// - alias: The alias of the user. +func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { + var r dataserviceapi.AliasAddress + + ep, err := url.JoinPath(config.CheckAliasURL, alias) + 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, err +} + +func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { + var okResponse api.OKResponse + var errResponse api.ErrResponse + + req.Header.Set("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/services/registration/_catch_swa b/services/registration/_catch_swa new file mode 100644 index 0000000..3affebd --- /dev/null +++ b/services/registration/_catch_swa @@ -0,0 +1 @@ +Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye. \ No newline at end of file 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 82e1fd4..2266160 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -6,10 +6,10 @@ 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 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/check_statement b/services/registration/check_statement new file mode 100644 index 0000000..0e989db --- /dev/null +++ b/services/registration/check_statement @@ -0,0 +1 @@ +Please enter your PIN to view statement: \ No newline at end of file diff --git a/services/registration/check_statement.vis b/services/registration/check_statement.vis new file mode 100644 index 0000000..d79b5ca --- /dev/null +++ b/services/registration/check_statement.vis @@ -0,0 +1,12 @@ +LOAD check_transactions 0 +RELOAD check_transactions +CATCH no_transfers flag_no_transfers 1 +LOAD authorize_account 6 +MOUT back 0 +MOUT quit 9 +HALT +RELOAD authorize_account +CATCH incorrect_pin flag_incorrect_pin 1 +INCMP _ 0 +INCMP quit 9 +INCMP transactions * diff --git a/services/registration/check_statement_swa b/services/registration/check_statement_swa new file mode 100644 index 0000000..468364f --- /dev/null +++ b/services/registration/check_statement_swa @@ -0,0 +1 @@ +Tafadhali weka PIN yako kuona taarifa ya matumizi: \ No newline at end of file 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_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/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/edit_family_name.vis b/services/registration/edit_family_name.vis new file mode 100644 index 0000000..590eab1 --- /dev/null +++ b/services/registration/edit_family_name.vis @@ -0,0 +1,18 @@ +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH update_familyname flag_allow_update 1 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info +MAP get_current_profile_info +MOUT back 0 +HALT +RELOAD set_back +CATCH _ flag_back_set 1 +LOAD save_familyname 64 +RELOAD save_familyname +CATCH pin_entry flag_familyname_set 1 +CATCH select_gender flag_gender_set 0 +CATCH edit_yob flag_yob_set 0 +CATCH edit_location flag_location_set 0 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_familyname_set 0 +INCMP select_gender * 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..6848b9c --- /dev/null +++ b/services/registration/edit_first_name.vis @@ -0,0 +1,18 @@ +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 +RELOAD set_back +CATCH _ flag_back_set 1 +LOAD save_firstname 128 +RELOAD save_firstname +CATCH pin_entry flag_firstname_set 1 +CATCH edit_family_name flag_familyname_set 0 +CATCH edit_gender flag_gender_set 0 +CATCH edit_yob flag_yob_set 0 +CATCH edit_location flag_location_set 0 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_firstname_set 0 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/edit_location.vis b/services/registration/edit_location.vis new file mode 100644 index 0000000..e4fcd8b --- /dev/null +++ b/services/registration/edit_location.vis @@ -0,0 +1,15 @@ +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH update_location flag_allow_update 1 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info +LOAD save_location 16 +MOUT back 0 +HALT +RELOAD set_back +CATCH _ flag_back_set 1 +RELOAD save_location +INCMP _ 0 +CATCH pin_entry flag_location_set 1 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_location_set 0 +INCMP edit_offerings * 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/edit_offerings.vis b/services/registration/edit_offerings.vis new file mode 100644 index 0000000..ddbc9e0 --- /dev/null +++ b/services/registration/edit_offerings.vis @@ -0,0 +1,14 @@ +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH update_offerings flag_allow_update 1 +LOAD get_current_profile_info 0 +RELOAD get_current_profile_info +LOAD save_offerings 8 +MOUT back 0 +HALT +RELOAD set_back +CATCH _ flag_back_set 1 +RELOAD save_offerings +INCMP _ 0 +CATCH pin_entry flag_offerings_set 1 +CATCH pin_entry flag_offerings_set 0 +INCMP update_profile_items * 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..e5ee12b 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 @@ -11,11 +11,12 @@ MOUT edit_offerings 6 MOUT view 7 MOUT back 0 HALT -INCMP my_account 0 -INCMP enter_name 1 -INCMP enter_familyname 2 +LOAD set_back 6 +INCMP ^ 0 +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/edit_yob.vis b/services/registration/edit_yob.vis new file mode 100644 index 0000000..255bea5 --- /dev/null +++ b/services/registration/edit_yob.vis @@ -0,0 +1,19 @@ +CATCH incorrect_pin flag_incorrect_pin 1 +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 +RELOAD set_back +CATCH _ flag_back_set 1 +LOAD verify_yob 6 +RELOAD verify_yob +CATCH incorrect_date_format flag_incorrect_date_format 1 +LOAD save_yob 32 +RELOAD save_yob +CATCH pin_entry flag_yob_set 1 +CATCH edit_location flag_location_set 0 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_yob_set 0 +INCMP edit_location * 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/english_menu b/services/registration/english_menu new file mode 100644 index 0000000..3d38949 --- /dev/null +++ b/services/registration/english_menu @@ -0,0 +1 @@ +English \ 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.vis b/services/registration/enter_familyname.vis deleted file mode 100644 index 5db4c17..0000000 --- a/services/registration/enter_familyname.vis +++ /dev/null @@ -1,8 +0,0 @@ -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH update_familyname flag_allow_update 1 -MOUT back 0 -HALT -LOAD save_familyname 0 -RELOAD save_familyname -INCMP _ 0 -INCMP pin_entry * diff --git a/services/registration/enter_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.vis b/services/registration/enter_location.vis deleted file mode 100644 index 8966872..0000000 --- a/services/registration/enter_location.vis +++ /dev/null @@ -1,8 +0,0 @@ -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH update_location flag_allow_update 1 -MOUT back 0 -HALT -LOAD save_location 0 -RELOAD save_location -INCMP _ 0 -INCMP pin_entry * diff --git a/services/registration/enter_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 f853d0a..0000000 --- a/services/registration/enter_name.vis +++ /dev/null @@ -1,8 +0,0 @@ -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH update_firstname flag_allow_update 1 -MOUT back 0 -HALT -LOAD save_firstname 0 -RELOAD save_firstname -INCMP _ 0 -INCMP pin_entry * diff --git a/services/registration/enter_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.vis b/services/registration/enter_offerings.vis deleted file mode 100644 index 5cc7977..0000000 --- a/services/registration/enter_offerings.vis +++ /dev/null @@ -1,8 +0,0 @@ -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH update_offerings flag_allow_update 1 -LOAD save_offerings 0 -MOUT back 0 -HALT -RELOAD save_offerings -INCMP _ 0 -INCMP pin_entry * 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.vis b/services/registration/enter_yob.vis deleted file mode 100644 index c74aeed..0000000 --- a/services/registration/enter_yob.vis +++ /dev/null @@ -1,10 +0,0 @@ -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH update_yob flag_allow_update 1 -MOUT back 0 -HALT -LOAD verify_yob 0 -CATCH incorrect_date_format flag_incorrect_date_format 1 -LOAD save_yob 0 -RELOAD save_yob -INCMP _ 0 -INCMP pin_entry * diff --git a/services/registration/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/incorrect_pin b/services/registration/incorrect_pin index d11ab54..7fcf610 100644 --- a/services/registration/incorrect_pin +++ b/services/registration/incorrect_pin @@ -1 +1 @@ -Incorrect pin \ No newline at end of file +Incorrect PIN \ No newline at end of file diff --git a/services/registration/invalid_others_pin b/services/registration/invalid_others_pin new file mode 100644 index 0000000..acdf45f --- /dev/null +++ b/services/registration/invalid_others_pin @@ -0,0 +1 @@ +The PIN you have entered is invalid.Please try a 4 digit number instead. \ No newline at end of file diff --git a/services/registration/invalid_others_pin.vis b/services/registration/invalid_others_pin.vis new file mode 100644 index 0000000..d218e6d --- /dev/null +++ b/services/registration/invalid_others_pin.vis @@ -0,0 +1,5 @@ +MOUT retry 1 +MOUT quit 9 +HALT +INCMP enter_others_new_pin 1 +INCMP quit 9 diff --git a/services/registration/invalid_pin_swa b/services/registration/invalid_pin_swa index 1817570..7512242 100644 --- a/services/registration/invalid_pin_swa +++ b/services/registration/invalid_pin_swa @@ -1 +1 @@ -PIN mpya na udhibitisho wa pin mpya hazilingani.Tafadhali jaribu tena.Kwa usaidizi piga simu +254757628885. +PIN mpya na udhibitisho wa PIN mpya hazilingani.Tafadhali jaribu tena.Kwa usaidizi piga simu +254757628885. 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/kiswahili_menu b/services/registration/kiswahili_menu new file mode 100644 index 0000000..e4d88a5 --- /dev/null +++ b/services/registration/kiswahili_menu @@ -0,0 +1 @@ +Kiswahili \ No newline at end of file diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po index ba9a9bb..4bf876b 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -12,3 +12,18 @@ msgstr "Kwa usaidizi zaidi,piga: 0757628885" msgid "Balance: %s\n" msgstr "Salio: %s\n" + +msgid "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" +msgstr "Salio la Kikundi: 0.00" + +msgid "Symbol: %s\nBalance: %s" +msgstr "Sarafu: %s\nSalio: %s" diff --git a/services/registration/main.vis b/services/registration/main.vis index 7e1c9bf..2982f47 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -1,10 +1,10 @@ LOAD set_default_voucher 8 RELOAD set_default_voucher -LOAD check_balance 64 -RELOAD check_balance LOAD check_vouchers 10 RELOAD check_vouchers -CATCH api_failure flag_api_call_error 1 +LOAD check_balance 128 +RELOAD check_balance +CATCH api_failure flag_api_call_error 1 MAP check_balance MOUT send 1 MOUT vouchers 2 diff --git a/services/registration/my_account.vis b/services/registration/my_account.vis index 43ee6a2..2b6289e 100644 --- a/services/registration/my_account.vis +++ b/services/registration/my_account.vis @@ -11,5 +11,6 @@ INCMP main 0 INCMP edit_profile 1 INCMP change_language 2 INCMP balances 3 +INCMP check_statement 4 INCMP pin_management 5 INCMP address 6 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_balance_swa b/services/registration/my_balance_swa index 9c3a7c7..afae8c1 100644 --- a/services/registration/my_balance_swa +++ b/services/registration/my_balance_swa @@ -1 +1 @@ -Salio lako ni: 0.00 SRF \ No newline at end of file +{{.check_balance}} \ No newline at end of file diff --git a/services/registration/my_vouchers.vis b/services/registration/my_vouchers.vis index b59441a..e79438e 100644 --- a/services/registration/my_vouchers.vis +++ b/services/registration/my_vouchers.vis @@ -6,3 +6,4 @@ MOUT back 0 HALT INCMP _ 0 INCMP select_voucher 1 +INCMP voucher_details 2 diff --git a/services/registration/next_menu b/services/registration/next_menu new file mode 100644 index 0000000..e2e838e --- /dev/null +++ b/services/registration/next_menu @@ -0,0 +1 @@ +Next \ No newline at end of file diff --git a/services/registration/next_menu_swa b/services/registration/next_menu_swa new file mode 100644 index 0000000..6511e40 --- /dev/null +++ b/services/registration/next_menu_swa @@ -0,0 +1 @@ +Mbele \ No newline at end of file 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_menu b/services/registration/no_menu index 54299a4..289cc91 100644 --- a/services/registration/no_menu +++ b/services/registration/no_menu @@ -1 +1 @@ -no \ No newline at end of file +No \ No newline at end of file diff --git a/services/registration/no_menu_swa b/services/registration/no_menu_swa index 3e6885e..a9d6b8d 100644 --- a/services/registration/no_menu_swa +++ b/services/registration/no_menu_swa @@ -1 +1 @@ -la \ No newline at end of file +La \ No newline at end of file diff --git a/services/registration/no_transfers b/services/registration/no_transfers new file mode 100644 index 0000000..3439806 --- /dev/null +++ b/services/registration/no_transfers @@ -0,0 +1 @@ +No transfers history \ No newline at end of file diff --git a/services/registration/no_transfers.vis b/services/registration/no_transfers.vis new file mode 100644 index 0000000..832ef22 --- /dev/null +++ b/services/registration/no_transfers.vis @@ -0,0 +1,5 @@ +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/no_transfers_swa b/services/registration/no_transfers_swa new file mode 100644 index 0000000..1f82e82 --- /dev/null +++ b/services/registration/no_transfers_swa @@ -0,0 +1 @@ +Hakuna historia kwa akaunti yako \ No newline at end of file diff --git a/services/registration/no_voucher b/services/registration/no_voucher index 332f00e..6303197 100644 --- a/services/registration/no_voucher +++ b/services/registration/no_voucher @@ -1 +1 @@ -You need a voucher to send \ No newline at end of file +You need a voucher to proceed \ No newline at end of file diff --git a/services/registration/no_voucher_swa b/services/registration/no_voucher_swa index 66e8f26..7291650 100644 --- a/services/registration/no_voucher_swa +++ b/services/registration/no_voucher_swa @@ -1 +1 @@ -Unahitaji sarafu kutuma \ No newline at end of file +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 ec0d8c1..26a8833 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -17,3 +17,14 @@ flag,flag_incorrect_date_format,23,this is set when the given year of birth is i 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 +flag,flag_no_transfers,29,this is set when a user does not have any transactions +flag,flag_incorrect_statement,30,this is set when the selected statement is invalid +flag,flag_firstname_set,31,this is set when the first name of the profile is set +flag,flag_familyname_set,32,this is set when the family name of the profile is set +flag,flag_yob_set,33,this is set when the yob of the profile is set +flag,flag_gender_set,34,this is set when the gender of the profile is set +flag,flag_location_set,35,this is set when the location of the profile is set +flag,flag_offerings_set,36,this is set when the offerings of the profile is set +flag,flag_back_set,37,this is set when it is a back navigation diff --git a/services/registration/prev_menu b/services/registration/prev_menu new file mode 100644 index 0000000..72d90d8 --- /dev/null +++ b/services/registration/prev_menu @@ -0,0 +1 @@ +Prev \ No newline at end of file diff --git a/services/registration/prev_menu_swa b/services/registration/prev_menu_swa new file mode 100644 index 0000000..e5a3e45 --- /dev/null +++ b/services/registration/prev_menu_swa @@ -0,0 +1 @@ +Nyuma \ No newline at end of file diff --git a/services/registration/profile_update_success.vis b/services/registration/profile_update_success.vis index a035093..f670e6e 100644 --- a/services/registration/profile_update_success.vis +++ b/services/registration/profile_update_success.vis @@ -1,3 +1,5 @@ +LOAD update_all_profile_items 0 +RELOAD update_all_profile_items MOUT back 0 MOUT quit 9 HALT 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.vis b/services/registration/select_voucher.vis index 08aa434..058d791 100644 --- a/services/registration/select_voucher.vis +++ b/services/registration/select_voucher.vis @@ -1,3 +1,4 @@ +CATCH no_voucher flag_no_active_voucher 1 LOAD get_vouchers 0 MAP get_vouchers MOUT back 0 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/send b/services/registration/send index d124026..306466c 100644 --- a/services/registration/send +++ b/services/registration/send @@ -1 +1 @@ -Enter recipient's phone number: \ No newline at end of file +Enter recipient's phone number/address/alias: \ No newline at end of file diff --git a/services/registration/send.vis b/services/registration/send.vis index 0ff0927..8928725 100644 --- a/services/registration/send.vis +++ b/services/registration/send.vis @@ -1,9 +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_eng.vis b/services/registration/set_eng.vis index 662fd2d..b66a1b7 100644 --- a/services/registration/set_eng.vis +++ b/services/registration/set_eng.vis @@ -1,3 +1,4 @@ LOAD set_language 6 +RELOAD set_language CATCH terms flag_account_created 0 MOVE language_changed diff --git a/services/registration/set_female.vis b/services/registration/set_female.vis index e211ada..da92520 100644 --- a/services/registration/set_female.vis +++ b/services/registration/set_female.vis @@ -1,4 +1,10 @@ -LOAD save_gender 0 +LOAD save_gender 32 +RELOAD save_gender CATCH incorrect_pin flag_incorrect_pin 1 CATCH update_gender flag_allow_update 1 -MOVE pin_entry +CATCH pin_entry flag_gender_set 1 +CATCH edit_yob flag_yob_set 0 +CATCH edit_location flag_location_set 0 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_gender_set 0 +MOVE edit_yob diff --git a/services/registration/set_male.vis b/services/registration/set_male.vis index e211ada..9a95937 100644 --- a/services/registration/set_male.vis +++ b/services/registration/set_male.vis @@ -1,4 +1,10 @@ -LOAD save_gender 0 +LOAD save_gender 16 +RELOAD save_gender CATCH incorrect_pin flag_incorrect_pin 1 CATCH update_gender flag_allow_update 1 -MOVE pin_entry +CATCH pin_entry flag_gender_set 1 +CATCH edit_yob flag_yob_set 0 +CATCH edit_location flag_location_set 0 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_gender_set 0 +MOVE edit_yob diff --git a/services/registration/set_swa.vis b/services/registration/set_swa.vis index 662fd2d..b66a1b7 100644 --- a/services/registration/set_swa.vis +++ b/services/registration/set_swa.vis @@ -1,3 +1,4 @@ LOAD set_language 6 +RELOAD set_language CATCH terms flag_account_created 0 MOVE language_changed diff --git a/services/registration/set_unspecified.vis b/services/registration/set_unspecified.vis index e211ada..824105c 100644 --- a/services/registration/set_unspecified.vis +++ b/services/registration/set_unspecified.vis @@ -1,4 +1,10 @@ -LOAD save_gender 0 +LOAD save_gender 8 +RELOAD save_gender CATCH incorrect_pin flag_incorrect_pin 1 CATCH update_gender flag_allow_update 1 -MOVE pin_entry +CATCH pin_entry flag_gender_set 1 +CATCH edit_yob flag_yob_set 0 +CATCH edit_location flag_location_set 0 +CATCH edit_offerings flag_offerings_set 0 +CATCH pin_entry flag_gender_set 0 +MOVE edit_yob diff --git a/services/registration/terms b/services/registration/terms index 05b8c11..8af5b06 100644 --- a/services/registration/terms +++ b/services/registration/terms @@ -1 +1,2 @@ -Do you agree to terms and conditions? \ No newline at end of file +Do you agree to terms and conditions? +https://grassecon.org/pages/terms-and-conditions diff --git a/services/registration/terms_swa b/services/registration/terms_swa index 7113cd7..5678186 100644 --- a/services/registration/terms_swa +++ b/services/registration/terms_swa @@ -1 +1,2 @@ -Kwa kutumia hii huduma umekubali sheria na masharti? \ No newline at end of file +Kwa kutumia hii huduma umekubali sheria na masharti? +https://grassecon.org/pages/terms-and-conditions diff --git a/services/registration/transactions b/services/registration/transactions new file mode 100644 index 0000000..8152c42 --- /dev/null +++ b/services/registration/transactions @@ -0,0 +1 @@ +{{.get_transactions}} \ No newline at end of file diff --git a/services/registration/transactions.vis b/services/registration/transactions.vis new file mode 100644 index 0000000..b21f1dc --- /dev/null +++ b/services/registration/transactions.vis @@ -0,0 +1,15 @@ +LOAD get_transactions 0 +MAP get_transactions +MOUT back 0 +MOUT quit 99 +MNEXT next 11 +MPREV prev 22 +HALT +LOAD view_statement 0 +RELOAD view_statement +CATCH . flag_incorrect_statement 1 +INCMP ^ 0 +INCMP quit 99 +INCMP > 11 +INCMP < 22 +INCMP view_statement * diff --git a/services/registration/transactions_swa b/services/registration/transactions_swa new file mode 100644 index 0000000..8152c42 --- /dev/null +++ b/services/registration/transactions_swa @@ -0,0 +1 @@ +{{.get_transactions}} \ No newline at end of file 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_profile_items.vis b/services/registration/update_profile_items.vis new file mode 100644 index 0000000..beda013 --- /dev/null +++ b/services/registration/update_profile_items.vis @@ -0,0 +1,3 @@ +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH profile_update_success flag_allow_update 1 +MOVE pin_entry diff --git a/services/registration/view_statement b/services/registration/view_statement new file mode 100644 index 0000000..1285cf9 --- /dev/null +++ b/services/registration/view_statement @@ -0,0 +1 @@ +{{.view_statement}} \ No newline at end of file diff --git a/services/registration/view_statement.vis b/services/registration/view_statement.vis new file mode 100644 index 0000000..4dde523 --- /dev/null +++ b/services/registration/view_statement.vis @@ -0,0 +1,10 @@ +MAP view_statement +MOUT back 0 +MOUT quit 9 +MNEXT next 11 +MPREV prev 22 +HALT +INCMP _ 0 +INCMP quit 9 +INCMP > 11 +INCMP < 22 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_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/yes_menu b/services/registration/yes_menu index 396a0ba..3fdfb3d 100644 --- a/services/registration/yes_menu +++ b/services/registration/yes_menu @@ -1 +1 @@ -yes \ No newline at end of file +Yes \ No newline at end of file diff --git a/services/registration/yes_menu_swa b/services/registration/yes_menu_swa index c5231fb..542d3c3 100644 --- a/services/registration/yes_menu_swa +++ b/services/registration/yes_menu_swa @@ -1 +1 @@ -ndio \ No newline at end of file +Ndio \ No newline at end of file