Compare commits

...

119 Commits

Author SHA1 Message Date
Carlosokumu
944e1841bb remove set session on read entry 2024-12-06 16:18:27 +03:00
Carlosokumu
b072c1314b set session on read entry 2024-12-06 15:57:22 +03:00
Carlosokumu
bab4a17bf2 remove setsession on userstore 2024-12-06 12:39:46 +03:00
43892f0d8c Merge pull request 'profile-edit-traverse' (#199) from profile-edit-traverse into master
Reviewed-on: urdt/ussd#199
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-05 16:37:37 +01:00
Carlosokumu
8e6b1e6f52 Merge branch 'master' into profile-edit-traverse 2024-12-05 18:34:29 +03:00
Carlosokumu
9cbbdff993 chore: use profileDataKeys as an array 2024-12-05 18:25:51 +03:00
316358765d Merge pull request 'data-items-cleanup' (#203) from data-items-cleanup into master
Reviewed-on: urdt/ussd#203
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-05 16:03:50 +01:00
alfred-mk
14737b5f12 Removed redundant naming of transfer related data 2024-12-05 17:58:51 +03:00
alfred-mk
caff27b43d Replace IntToBytes(value int) and ToBytes() with a single ToBytes() function 2024-12-05 17:50:40 +03:00
alfred-mk
589a94216b Use the DATATYPE_USERDATA as the prefix for the NewSubPrefixDb func 2024-12-05 17:02:26 +03:00
alfred-mk
a659fb06fa Added a function to convert int to []byte 2024-12-05 16:58:03 +03:00
alfred-mk
f733fe5636 Start the sub prefix data at 256 (0x0100) 2024-12-05 16:31:47 +03:00
alfred-mk
18423fcd9c updated the name of the voucher related data 2024-12-05 16:26:56 +03:00
Carlosokumu
72a3681767 add test data. 2024-12-05 16:03:08 +03:00
Carlosokumu
160ccbb220 read test file from test run arg 2024-12-05 14:05:32 +03:00
Carlosokumu
22f96363ba add test files 2024-12-05 14:04:56 +03:00
Carlosokumu
3e7f90733e update test names 2024-12-05 12:22:29 +03:00
Carlosokumu
321f038c7c iterate over a map for the set profile items 2024-12-05 10:52:45 +03:00
alfred-mk
7a9de79aae return nil with error 2024-12-04 21:07:11 +03:00
alfred-mk
862830e9de renamed internal/storage/db.go -> internal/storage/sub_prefix_db.go for clarity 2024-12-04 20:59:46 +03:00
alfred-mk
bc0e536d3d updated failing tests 2024-12-04 20:55:03 +03:00
alfred-mk
82884a75a3 Merge branch 'master' into data-items-cleanup 2024-12-04 20:45:38 +03:00
alfred-mk
93c44861e0 Use numeric prefixes 2024-12-04 20:42:47 +03:00
alfred-mk
4ecfc9de38 removed DATATYPE_USERSUB and replaced with DATATYPE_USERDATA 2024-12-04 20:38:52 +03:00
Carlosokumu
a84c3e0852 update menu traversal test data 2024-12-04 09:46:16 +03:00
Carlosokumu
8efed966a0 set flag when location is set 2024-12-04 09:08:47 +03:00
Carlosokumu
e7c4b5bca7 update tests 2024-12-04 09:08:25 +03:00
Carlosokumu
ed632248c5 add doc lines and check for back naviagtions 2024-12-04 08:30:07 +03:00
Carlosokumu
5c202741d6 add handler for catching back navigations 2024-12-04 08:20:43 +03:00
Carlosokumu
c5ebdbf85b catch back navigations 2024-12-04 08:20:09 +03:00
Carlosokumu
c4282a870e add flag to catch back navigations 2024-12-04 08:19:20 +03:00
Carlosokumu
91cd6077ce Merge branch 'master' into profile-edit-traverse 2024-12-03 22:25:47 +03:00
Carlosokumu
c0ed6fa9c8 catch incorrect pin entries 2024-12-03 21:43:24 +03:00
22c9c3e0f0 Merge pull request 'minor-bug-fixes' (#177) from minor-bug-fixes into master
Reviewed-on: urdt/ussd#177
2024-12-03 18:18:23 +01:00
a709d24520 Merge branch 'master' into minor-bug-fixes 2024-12-03 18:16:19 +01:00
e56138e416 Merge pull request 'Clear persister from handler in outer code aswell' (#200) from lash/persister-freakout into master
Reviewed-on: urdt/ussd#200
2024-12-03 17:18:11 +01:00
lash
d516584d90 Clear persister from handler in outer code aswell 2024-12-03 16:16:53 +00:00
Carlosokumu
b420a9bba0 set flag if profile data is set 2024-12-03 17:41:06 +03:00
Carlosokumu
a20ab79355 explicit reload save gender 2024-12-03 17:38:20 +03:00
Carlosokumu
e0ec15b272 allow sequential profile edit 2024-12-03 14:40:57 +03:00
Carlosokumu
9e998f9a29 add a zero pad value to unfilled profile item 2024-12-03 14:37:55 +03:00
Carlosokumu
1c7c0af712 catch next unset profile item 2024-12-03 14:36:48 +03:00
alfred-mk
d40a4a171f formatted code 2024-12-03 14:12:47 +03:00
alfred-mk
ba430a5849 add a separate function to handle ConstructName 2024-12-03 14:10:05 +03:00
alfred-mk
0f21b01813 resolved error in the TestViewVoucher 2024-12-03 13:37:00 +03:00
alfred-mk
10586baf0d resolved error in the TestCheckBalance 2024-12-03 13:35:14 +03:00
alfred-mk
e979742424 resolved error in the TestValidateRecipient 2024-12-03 13:32:18 +03:00
alfred-mk
ff3f049226 updated the CheckAliasAddress mock 2024-12-03 13:31:30 +03:00
alfred-mk
13b45c49da Merge branch 'master' into minor-bug-fixes 2024-12-03 12:58:26 +03:00
Carlosokumu
a72fb08dc8 allow sequential profile edit 2024-12-03 11:20:35 +03:00
Carlosokumu
944fa89b3c add profile holder struct 2024-12-03 11:19:38 +03:00
Carlosokumu
48e1b02e0e allow all item profile edit 2024-12-02 20:50:21 +03:00
Carlosokumu
3e0bbe5ffe add handler to update profile items 2024-12-02 20:35:56 +03:00
Carlosokumu
fd586d09c3 add required profile edit flags 2024-12-02 20:35:04 +03:00
b615c27cf6 Merge pull request 'trigger-balance-reload' (#193) from trigger-balance-reload into master
Reviewed-on: urdt/ussd#193
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-02 16:36:26 +01:00
22e870b3e5 Merge branch 'master' into trigger-balance-reload 2024-12-02 16:36:08 +01:00
da462346f9 Merge pull request 'Always reset persister in handler' (#194) from lash/no-persister-deadlock into master
Reviewed-on: urdt/ussd#194
2024-12-02 16:17:36 +01:00
lash
406bd84875 Always reset persister in handler 2024-12-02 14:53:18 +00:00
alfred-mk
419cd185fc Merge branch 'master' into minor-bug-fixes 2024-12-02 15:25:34 +03:00
7976e237ca Merge pull request 'voucher-details' (#179) from voucher-details into master
Reviewed-on: urdt/ussd#179
Reviewed-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-12-02 11:43:30 +01:00
alfred-mk
19ec8f0817 Merge branch 'master' into voucher-details 2024-12-02 13:32:32 +03:00
alfred-mk
ef3a3d6717 updated the TestGetVoucherDetails 2024-12-02 13:30:33 +03:00
alfred-mk
c2019267d1 capitalize the voucher descriptions 2024-12-02 13:26:46 +03:00
Carlosokumu
0091fbcabb fix: looped navigation 2024-12-02 09:03:04 +03:00
alfred-mk
aa7497573e removed unused code 2024-11-30 15:29:28 +03:00
alfred-mk
54c1fe51ef update the active voucher data when checking the current vouchers 2024-11-30 15:28:21 +03:00
alfred-mk
7a86b2ad3b updated the UpdateVoucherData description 2024-11-30 15:26:13 +03:00
alfred-mk
6b23c284e5 check vouchers before checking the balance 2024-11-30 15:24:14 +03:00
alfred-mk
aab6660edd Capitalize menu items 2024-11-29 15:39:27 +03:00
alfred-mk
c46f41e25f Format the balance to 2 decimal places 2024-11-29 14:47:22 +03:00
Carlosokumu
00c0445eed show name without depending on family name being set 2024-11-28 11:42:47 +03:00
alfred-mk
c8c6b05b8a Merge branch 'master' into minor-bug-fixes 2024-11-26 15:31:45 +03:00
alfred-mk
08ff1056d7 Validate aliases, addresses and phone numbers in the send menu (#176)
- Update the phone number regex
- Check whether the recipient is a valid phone number, alias or address

Reviewed-on: urdt/ussd#176
Co-authored-by: alfred-mk <alfredmwaik@gmail.com>
Co-committed-by: alfred-mk <alfredmwaik@gmail.com>
2024-11-26 07:24:57 +01:00
0f4a7e900f Merge pull request 'feat: upgrade go-vise dep, minor cleanups to go.mod' (#182) from sohail/upgrade-deps into master
Reviewed-on: urdt/ussd#182
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-11-26 07:02:07 +01:00
2d6c434bde feat: upgrade go-vise dep, minor cleanups to go.mod
* added stub for Dump()
2024-11-25 09:56:24 +03:00
f5d2644031 Merge pull request 'account-statement' (#126) from account-statement into master
Reviewed-on: urdt/ussd#126
2024-11-22 19:24:08 +01:00
09b4fa2860 Merge branch 'master' into account-statement 2024-11-22 19:23:59 +01:00
a748c1b6b2 chore: fix old reference 2024-11-22 11:53:13 +03:00
Carlosokumu
a17cf78d29 Merge remote-tracking branch 'refs/remotes/origin/minor-bug-fixes' into minor-bug-fixes 2024-11-22 11:35:57 +03:00
Carlosokumu
9847433e0a use _ for back navigation 2024-11-22 11:30:13 +03:00
alfred-mk
7ce50398d1 use the language translation instead of hardcoded eng 2024-11-21 15:54:00 +03:00
alfred-mk
e30bc177e9 fixed typo and added a new translation 2024-11-21 15:52:07 +03:00
alfred-mk
b9ff467c0c use the correct balance 2024-11-21 15:51:04 +03:00
Carlosokumu
1174500e3f add test for voucher details 2024-11-21 15:19:36 +03:00
alfred-mk
07df450b3c include labels to define the symbol and balance while selecting a voucher 2024-11-21 15:15:15 +03:00
Carlosokumu
b8d938d3aa add voucher details 2024-11-21 13:04:19 +03:00
Carlosokumu
d1e9340ea9 add voucher details 2024-11-21 13:03:43 +03:00
Carlosokumu
8925e26c4c refactor check for valid yob 2024-11-21 11:25:47 +03:00
Carlosokumu
9b89462797 add function to check validity of provided yob 2024-11-21 11:21:16 +03:00
alfred-mk
51bad64a51 Merge branch 'master' into account-statement 2024-11-20 19:00:14 +03:00
Carlosokumu
7880294c6f set eng as default language 2024-11-20 17:14:25 +03:00
Carlosokumu
451b15fb6b explicit set_language reload 2024-11-20 17:13:14 +03:00
Carlosokumu
d20700ca74 fix size limit error 2024-11-20 16:39:50 +03:00
Carlosokumu
9cf1cbe425 add swahili translation for catch node 2024-11-20 16:36:18 +03:00
212cd48249 debug: postgres conn string 2024-11-19 17:53:06 +03:00
7adc0c9c08 fix: Dockerfile to include .env 2024-11-19 17:25:20 +03:00
0a19a6c48b fix: github ci spec 2024-11-19 17:18:13 +03:00
66b5843b0d feat: dockerfile and github based CI build for Africastalking variant (#174)
## Summary

* fixed missing error handler in main
* add injectable build string in main
* add (dynamically linked) docker build
* add github CI workflow
* add extra but useful dev files in dev folder

* closes #93

## Notes

* We'll move to a self-hosted CI runner once it is installed on Gitea.

Reviewed-on: urdt/ussd#174
Co-authored-by: Mohammed Sohail <sohailsameja@gmail.com>
Co-committed-by: Mohammed Sohail <sohailsameja@gmail.com>
2024-11-19 15:15:24 +01:00
alfred-mk
df89fe69e1 return to the main node 2024-11-19 17:09:59 +03:00
alfred-mk
9498242901 removed debug statement 2024-11-19 16:27:19 +03:00
alfred-mk
eab6dbd74c Check if index is within range 2024-11-19 16:23:35 +03:00
alfred-mk
6159686a8e update function comment 2024-11-19 16:21:55 +03:00
alfred-mk
1d82bc2261 added transactions functions 2024-11-19 16:07:28 +03:00
alfred-mk
bfcdd79f33 view single statement 2024-11-19 16:04:46 +03:00
alfred-mk
d49b68597c check transactions and catch when none exist 2024-11-19 16:03:54 +03:00
alfred-mk
1d9f4fc13e added statement related flags 2024-11-19 15:50:19 +03:00
alfred-mk
6fa0d8e2ff Merge branch 'master' into account-statement 2024-11-19 10:33:09 +03:00
carlos
109a2a2020 Merge pull request 'menu-balances' (#173) from menu-balances into master
Reviewed-on: urdt/ussd#173
Reviewed-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-11-19 08:18:43 +01:00
alfred-mk
bc6d8098f3 updated the failed test 2024-11-19 10:15:34 +03:00
Carlosokumu
01e75e9217 update test 2024-11-18 17:30:34 +03:00
Carlosokumu
f2b17880ba check community and my balance separately 2024-11-18 17:30:09 +03:00
Carlosokumu
1d4f116079 community balance str 2024-11-18 17:23:59 +03:00
Carlosokumu
44c52b6ed7 rename fetch_custodial_balances -> fetch_community_balance 2024-11-18 17:23:36 +03:00
Carlosokumu
454f67b317 show balance based on current voucher 2024-11-18 17:21:04 +03:00
Carlosokumu
e1506a3dcf show a placehlolder community balance 2024-11-18 17:20:12 +03:00
carlos
b22a4adec1 Merge pull request 'updated send node' (#172) from send-node into master
Reviewed-on: urdt/ussd#172
2024-11-18 14:19:42 +01:00
alfred-mk
fc0043e3f6 Adde the transactions node 2024-10-23 14:52:15 +03:00
alfred-mk
d41ba79ae4 Adde the check_statement node 2024-10-23 14:51:59 +03:00
alfred-mk
f36847d966 Added a placeholder function to get transactions 2024-10-23 14:51:17 +03:00
90 changed files with 1741 additions and 352 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
/**
!/cmd/africastalking
!/common
!/config
!/initializers
!/internal
!/models
!/remote
!/services
!/LICENSE
!/README.md
!/go.*
!/.env.example

56
.github/workflows/docker.yaml vendored Normal file
View File

@@ -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 }}

41
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -20,6 +20,7 @@ import (
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
@@ -29,8 +30,10 @@ import (
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
build = "dev"
) )
func init() { func init() {
@@ -42,14 +45,14 @@ type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) { func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
log.Println("got an invalid request:", rq) log.Printf("got an invalid request:", rq)
return "", handlers.ErrInvalidRequest return "", handlers.ErrInvalidRequest
} }
// Capture body (if any) for logging // Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body) body, err := io.ReadAll(rqv.Body)
if err != nil { if err != nil {
log.Println("failed to read request body:", err) log.Printf("failed to read request body:", err)
return "", fmt.Errorf("failed to read request body: %v", err) return "", fmt.Errorf("failed to read request body: %v", err)
} }
// Reset the body for further reading // Reset the body for further reading
@@ -59,13 +62,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
bodyLog := map[string]string{"body": string(body)} bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog) logBytes, err := json.Marshal(bodyLog)
if err != nil { if err != nil {
log.Println("failed to marshal request body:", err) log.Printf("failed to marshal request body:", err)
} else { } else {
log.Println("Received request:", string(logBytes)) log.Printf("Received request:", string(logBytes))
} }
if err := rqv.ParseForm(); err != nil { if err := rqv.ParseForm(); err != nil {
log.Println("failed to parse form data: %v", err) log.Printf("failed to parse form data: %v", err)
return "", fmt.Errorf("failed to parse form data: %v", err) return "", fmt.Errorf("failed to parse form data: %v", err)
} }
@@ -74,7 +77,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
return "", fmt.Errorf("no phone number found") return "", fmt.Errorf("no phone number found")
} }
return phoneNumber, nil formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil {
fmt.Printf("Error: %v\n", err)
return "", fmt.Errorf("failed to format number")
}
return formattedNumber, nil
} }
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) { func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
@@ -115,7 +124,7 @@ func main() {
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() 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.Background()
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
@@ -157,6 +166,10 @@ func main() {
} }
lhs, err := handlers.NewLocalHandlerService(ctx, 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) lhs.SetDataStore(&userdataStore)
if err != nil { if err != nil {

View File

@@ -32,7 +32,19 @@ const (
DATA_PUBLIC_KEY_REVERSE DATA_PUBLIC_KEY_REVERSE
DATA_ACTIVE_DECIMAL DATA_ACTIVE_DECIMAL
DATA_ACTIVE_ADDRESS DATA_ACTIVE_ADDRESS
DATA_TRANSACTIONS // Start the sub prefix data at 256 (0x0100)
DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
DATA_VOUCHER_BALANCES
DATA_VOUCHER_DECIMALS
DATA_VOUCHER_ADDRESSES
DATA_TX_SENDERS
DATA_TX_RECIPIENTS
DATA_TX_VALUES
DATA_TX_ADDRESSES
DATA_TX_HASHES
DATA_TX_DATES
DATA_TX_SYMBOLS
DATA_TX_DECIMALS
) )
var ( var (
@@ -69,3 +81,10 @@ func StringToDataTyp(str string) (DataTyp, error) {
return 0, errors.New("invalid DataTyp string") 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
}

73
common/recipient.go Normal file
View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -19,7 +19,6 @@ type UserDataStore struct {
// ReadEntry retrieves an entry to the userdata store. // ReadEntry retrieves an entry to the userdata store.
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId)) k := PackKey(typ, []byte(sessionId))
return store.Get(ctx, k) return store.Get(ctx, k)
} }
@@ -28,7 +27,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
// BUG: this uses sessionId twice // BUG: this uses sessionId twice
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId)) k := PackKey(typ, []byte(sessionId))
return store.Put(ctx, k, value) return store.Put(ctx, k, value)
} }

View File

@@ -64,22 +64,23 @@ func ScaleDownBalance(balance, decimals string) string {
// GetVoucherData retrieves and matches voucher data // GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []string{"sym", "bal", "deci", "addr"} keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
data := make(map[string]string) data := make(map[DataTyp]string)
for _, key := range keys { for _, key := range keys {
value, err := db.Get(ctx, []byte(key)) value, err := db.Get(ctx, ToBytes(key))
if err != nil { 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) data[key] = string(value)
} }
symbol, balance, decimal, address := MatchVoucher(input, symbol, balance, decimal, address := MatchVoucher(input,
data["sym"], data[DATA_VOUCHER_SYMBOLS],
data["bal"], data[DATA_VOUCHER_BALANCES],
data["deci"], data[DATA_VOUCHER_DECIMALS],
data["addr"]) data[DATA_VOUCHER_ADDRESSES],
)
if symbol == "" { if symbol == "" {
return nil, nil return nil, nil
@@ -151,7 +152,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
return data, nil 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 { func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.TraceCtxf(ctx, "dtal", "data", data) logg.TraceCtxf(ctx, "dtal", "data", data)
// Active voucher data entries // Active voucher data entries

View File

@@ -8,8 +8,9 @@ import (
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"git.grassecon.net/urdt/ussd/internal/storage" visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem" memdb "git.defalsify.org/vise.git/db/mem"
"git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
@@ -83,19 +84,21 @@ func TestGetVoucherData(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
prefix := ToBytes(visedb.DATATYPE_USERDATA)
spdb := storage.NewSubPrefixDb(db, prefix)
// Test voucher data // Test voucher data
mockData := map[string][]byte{ mockData := map[DataTyp][]byte{
"sym": []byte("1:SRF\n2:MILO"), DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"), DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"), DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
} }
// Put the data // Put the data
for key, value := range mockData { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -15,6 +15,7 @@ const (
voucherHoldingsPathPrefix = "/api/v1/holdings" voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10" voucherTransfersPathPrefix = "/api/v1/transfers/last10"
voucherDataPathPrefix = "/api/v1/token" voucherDataPathPrefix = "/api/v1/token"
AliasPrefix = "api/v1/alias"
) )
var ( var (
@@ -32,6 +33,7 @@ var (
VoucherHoldingsURL string VoucherHoldingsURL string
VoucherTransfersURL string VoucherTransfersURL string
VoucherDataURL string VoucherDataURL string
CheckAliasURL string
) )
func setBase() error { func setBase() error {
@@ -66,6 +68,7 @@ func LoadConfig() error {
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
return nil return nil
} }

View File

@@ -0,0 +1,3 @@
url: http://localhost:7123
dial: "*384*96#"
phoneNumber: +254722123456

21
dev/docker-compose.yaml Normal file
View File

@@ -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

1
dev/init_db.sql Normal file
View File

@@ -0,0 +1 @@
CREATE DATABASE urdt_ussd;

36
go.mod
View File

@@ -2,29 +2,16 @@ module git.grassecon.net/urdt/ussd
go 1.23.0 go 1.23.0
toolchain go1.23.2
require ( require (
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a
github.com/alecthomas/assert/v2 v2.2.2 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/grassrootseconomics/eth-custodial v1.3.0-beta
github.com/peteole/testdata-loader v0.3.0 github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
)
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/kr/text v0.2.0 // indirect github.com/peteole/testdata-loader v0.3.0
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.27.0 // indirect gopkg.in/leonelquinteros/gotext.v1 v1.3.1
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
) )
require ( require (
@@ -33,13 +20,20 @@ require (
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // 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/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // 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/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4 // indirect 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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )

8
go.sum
View File

@@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE=
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= 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/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= 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/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 h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= 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 v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
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/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 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View File

@@ -55,6 +55,9 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
} }
f.hn = f.hn.WithPersister(rqs.Storage.Persister) f.hn = f.hn.WithPersister(rqs.Storage.Persister)
defer func() {
f.hn.Exit()
}()
eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
en, ok := eni.(*engine.DefaultEngine) en, ok := eni.(*engine.DefaultEngine)
if !ok { if !ok {

View File

@@ -103,7 +103,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) 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("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
@@ -118,6 +118,11 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) 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 return ussdHandlers, nil
} }

View File

@@ -10,7 +10,6 @@ import (
"strings" "strings"
"git.defalsify.org/vise.git/asm" "git.defalsify.org/vise.git/asm"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
@@ -21,26 +20,31 @@ import (
"git.defalsify.org/vise.git/state" "git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/common" "git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/internal/utils" "git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/models"
"git.grassecon.net/urdt/ussd/remote" "git.grassecon.net/urdt/ussd/remote"
"gopkg.in/leonelquinteros/gotext.v1" "gopkg.in/leonelquinteros/gotext.v1"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
var ( var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler") logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale") translationDir = path.Join(scriptDir, "locale")
okResponse *api.OKResponse
errResponse *api.ErrResponse
) )
// Define the regex patterns as constants // Define the regex patterns as constants
const ( const (
phoneRegex = `(\(\d{3}\)\s?|\d{3}[-.\s]?)?\d{3}[-.\s]?\d{4}`
pinPattern = `^\d{4}$` 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 // FlagManager handles centralized flag management
type FlagManager struct { type FlagManager struct {
parser *asm.FlagParser parser *asm.FlagParser
@@ -73,6 +77,7 @@ type Handlers struct {
flagManager *asm.FlagParser flagManager *asm.FlagParser
accountService remote.AccountServiceInterface accountService remote.AccountServiceInterface
prefixDb storage.PrefixDb prefixDb storage.PrefixDb
profile *models.Profile
} }
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) { func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) {
@@ -82,8 +87,10 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
userDb := &common.UserDataStore{ userDb := &common.UserDataStore{
Db: 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{ h := &Handlers{
userdataStore: userDb, userdataStore: userDb,
@@ -91,21 +98,11 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
adminstore: adminstore, adminstore: adminstore,
accountService: accountService, accountService: accountService,
prefixDb: prefixDb, prefixDb: prefixDb,
profile: &models.Profile{Max: 6},
} }
return h, nil return h, nil
} }
// isValidPIN checks whether the given input is a 4 digit number
func isValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
func isValidPhoneNumber(phonenumber string) bool {
match, _ := regexp.MatchString(phoneRegex, phonenumber)
return match
}
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
if h.pe != nil { if h.pe != nil {
panic("persister already set") panic("persister already set")
@@ -120,6 +117,9 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca) logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca)
return r, nil return r, nil
} }
defer func() {
h.Exit()
}()
h.st = h.pe.GetState() h.st = h.pe.GetState()
h.ca = h.pe.GetMemory() h.ca = h.pe.GetMemory()
@@ -139,13 +139,16 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca) logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca)
return r, fmt.Errorf("cannot get state and memory for handler") 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) logg.DebugCtxf(ctx, "handler has been initialized", "state", h.st, "cache", h.ca)
return r, nil return r, nil
} }
func (h *Handlers) Exit() {
h.pe = nil
}
// SetLanguage sets the language across the menu // SetLanguage sets the language across the menu
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@@ -154,7 +157,8 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
code := strings.Split(symbol, "_")[1] code := strings.Split(symbol, "_")[1]
if !utils.IsValidISO639(code) { if !utils.IsValidISO639(code) {
return res, nil //Fallback to english instead?
code = "eng"
} }
res.FlagSet = append(res.FlagSet, state.FLAG_LANG) res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
res.Content = code res.Content = code
@@ -412,7 +416,10 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte)
firstName := string(input) firstName := string(input)
store := h.userdataStore store := h.userdataStore
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") 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) allowUpdate := h.st.MatchFlag(flag_allow_update, true)
firstNameSet := h.st.MatchFlag(flag_firstname_set, true)
if allowUpdate { if allowUpdate {
temporaryFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) temporaryFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE)
err = store.WriteEntry(ctx, sessionId, common.DATA_FIRST_NAME, []byte(temporaryFirstName)) err = store.WriteEntry(ctx, sessionId, common.DATA_FIRST_NAME, []byte(temporaryFirstName))
@@ -420,11 +427,16 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte)
logg.ErrorCtxf(ctx, "failed to write firstName entry with", "key", common.DATA_FIRST_NAME, "value", temporaryFirstName, "error", err) logg.ErrorCtxf(ctx, "failed to write firstName entry with", "key", common.DATA_FIRST_NAME, "value", temporaryFirstName, "error", err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_firstname_set)
} else { } else {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(firstName)) if firstNameSet {
if err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(firstName))
logg.ErrorCtxf(ctx, "failed to write temporaryFirstName entry with", "key", common.DATA_TEMPORARY_VALUE, "value", firstName, "error", err) if err != nil {
return res, err 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)
} }
} }
@@ -444,7 +456,9 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte)
familyName := string(input) familyName := string(input)
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") 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) allowUpdate := h.st.MatchFlag(flag_allow_update, true)
familyNameSet := h.st.MatchFlag(flag_familyname_set, true)
if allowUpdate { if allowUpdate {
temporaryFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) temporaryFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE)
@@ -453,11 +467,16 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte)
logg.ErrorCtxf(ctx, "failed to write familyName entry with", "key", common.DATA_FAMILY_NAME, "value", temporaryFamilyName, "error", err) logg.ErrorCtxf(ctx, "failed to write familyName entry with", "key", common.DATA_FAMILY_NAME, "value", temporaryFamilyName, "error", err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_familyname_set)
} else { } else {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(familyName)) if familyNameSet {
if err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(familyName))
logg.ErrorCtxf(ctx, "failed to write temporaryFamilyName entry with", "key", common.DATA_TEMPORARY_VALUE, "value", familyName, "error", err) if err != nil {
return res, err 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)
} }
} }
@@ -475,7 +494,10 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
yob := string(input) yob := string(input)
store := h.userdataStore store := h.userdataStore
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") 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) allowUpdate := h.st.MatchFlag(flag_allow_update, true)
yobSet := h.st.MatchFlag(flag_yob_set, true)
if allowUpdate { if allowUpdate {
temporaryYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) temporaryYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE)
@@ -484,11 +506,16 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
logg.ErrorCtxf(ctx, "failed to write yob entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryYob, "error", err) logg.ErrorCtxf(ctx, "failed to write yob entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryYob, "error", err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_yob_set)
} else { } else {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(yob)) if yobSet {
if err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(yob))
logg.ErrorCtxf(ctx, "failed to write temporaryYob entry with", "key", common.DATA_TEMPORARY_VALUE, "value", yob, "error", err) if err != nil {
return res, err 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)
} }
} }
@@ -507,7 +534,9 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (
store := h.userdataStore store := h.userdataStore
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") 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) allowUpdate := h.st.MatchFlag(flag_allow_update, true)
locationSet := h.st.MatchFlag(flag_location_set, true)
if allowUpdate { if allowUpdate {
temporaryLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) temporaryLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE)
@@ -516,11 +545,17 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (
logg.ErrorCtxf(ctx, "failed to write location entry with", "key", common.DATA_LOCATION, "value", temporaryLocation, "error", err) logg.ErrorCtxf(ctx, "failed to write location entry with", "key", common.DATA_LOCATION, "value", temporaryLocation, "error", err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_location_set)
} else { } else {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(location)) if locationSet {
if err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(location))
logg.ErrorCtxf(ctx, "failed to write temporaryLocation entry with", "key", common.DATA_TEMPORARY_VALUE, "value", location, "error", err) if err != nil {
return res, err 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)
} }
} }
@@ -539,7 +574,10 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
gender := strings.Split(symbol, "_")[1] gender := strings.Split(symbol, "_")[1]
store := h.userdataStore store := h.userdataStore
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") 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) allowUpdate := h.st.MatchFlag(flag_allow_update, true)
genderSet := h.st.MatchFlag(flag_gender_set, true)
if allowUpdate { if allowUpdate {
temporaryGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) temporaryGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE)
@@ -548,11 +586,16 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
logg.ErrorCtxf(ctx, "failed to write gender entry with", "key", common.DATA_GENDER, "value", gender, "error", err) logg.ErrorCtxf(ctx, "failed to write gender entry with", "key", common.DATA_GENDER, "value", gender, "error", err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_gender_set)
} else { } else {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(gender)) if genderSet {
if err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(gender))
logg.ErrorCtxf(ctx, "failed to write temporaryGender entry with", "key", common.DATA_TEMPORARY_VALUE, "value", gender, "error", err) if err != nil {
return res, err 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)
} }
} }
@@ -572,7 +615,10 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
store := h.userdataStore store := h.userdataStore
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") 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) allowUpdate := h.st.MatchFlag(flag_allow_update, true)
offeringsSet := h.st.MatchFlag(flag_offerings_set, true)
if allowUpdate { if allowUpdate {
temporaryOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE) temporaryOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE)
@@ -581,11 +627,16 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
logg.ErrorCtxf(ctx, "failed to write offerings entry with", "key", common.DATA_TEMPORARY_VALUE, "value", offerings, "error", err) logg.ErrorCtxf(ctx, "failed to write offerings entry with", "key", common.DATA_TEMPORARY_VALUE, "value", offerings, "error", err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_offerings_set)
} else { } else {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(offerings)) if offeringsSet {
if err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(offerings))
logg.ErrorCtxf(ctx, "failed to write temporaryOfferings entry with", "key", common.DATA_TEMPORARY_VALUE, "value", offerings, "error", err) if err != nil {
return res, err 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)
} }
} }
@@ -678,6 +729,18 @@ func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []by
return res, nil 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 // CheckAccountStatus queries the API using the TrackingId and sets flags
// based on the account status // based on the account status
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
@@ -763,12 +826,11 @@ func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (res
return res, nil return res, nil
} }
if len(date) == 4 { if utils.IsValidYOb(date) {
res.FlagReset = append(res.FlagReset, flag_incorrect_date_format) res.FlagReset = append(res.FlagReset, flag_incorrect_date_format)
} else { } else {
res.FlagSet = append(res.FlagSet, flag_incorrect_date_format) res.FlagSet = append(res.FlagSet, flag_incorrect_date_format)
} }
return res, nil return res, nil
} }
@@ -817,47 +879,29 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
return res, 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 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 var res resource.Result
code := codeFromCtx(ctx)
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
sessionId, ok := ctx.Value("SessionId").(string) //TODO:
if !ok { //Check if the address is a community account,if then,get the actual balance
return res, fmt.Errorf("missing session") res.Content = l.Get("Community Balance: 0.00")
}
symbol, _ := h.st.Where()
balanceType := strings.Split(symbol, "_")[0]
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Balance
switch balanceType {
case "my":
res.Content = fmt.Sprintf("Your balance is %s", balance)
case "community":
res.Content = fmt.Sprintf("Your community balance is %s", balance)
default:
break
}
return res, nil return res, nil
} }
@@ -905,7 +949,7 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input
} }
blockedNumber := string(input) blockedNumber := string(input)
_, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY) _, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY)
if !isValidPhoneNumber(blockedNumber) { if !common.IsValidPhoneNumber(blockedNumber) {
res.FlagSet = append(res.FlagSet, flag_unregistered_number) res.FlagSet = append(res.FlagSet, flag_unregistered_number)
return res, nil return res, nil
} }
@@ -926,10 +970,9 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input
return res, nil return res, nil
} }
// ValidateRecipient validates that the given input is a valid phone number. // ValidateRecipient validates that the given input is valid.
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
store := h.userdataStore store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
@@ -937,13 +980,16 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
recipient := string(input)
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") 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)
if recipient != "0" { if recipient != "0" {
if !isValidPhoneNumber(recipient) { 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.FlagSet = append(res.FlagSet, flag_invalid_recipient)
res.Content = recipient res.Content = recipient
@@ -957,25 +1003,61 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
return res, err return res, err
} }
publicKey, err := store.ReadEntry(ctx, recipient, common.DATA_PUBLIC_KEY) switch recipientType {
if err != nil { case "phone number":
if db.IsNotFound(err) { // format the phone number
logg.InfoCtxf(ctx, "Unregistered number") formattedNumber, err := common.FormatPhoneNumber(recipient)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err)
res.Content = recipient return res, err
return res, nil
} }
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) // Check if the phone number is registered
return res, err 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
}
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey) logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
if err != nil { return res, err
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err) }
return res, nil
// 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
}
} }
} }
@@ -1258,6 +1340,17 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
var res resource.Result var res resource.Result
var profileInfo []byte var profileInfo []byte
var err error 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) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
@@ -1284,6 +1377,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err) logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_firstname_set)
res.Content = string(profileInfo) res.Content = string(profileInfo)
case common.DATA_FAMILY_NAME: case common.DATA_FAMILY_NAME:
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
@@ -1295,6 +1389,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err) logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_familyname_set)
res.Content = string(profileInfo) res.Content = string(profileInfo)
case common.DATA_GENDER: case common.DATA_GENDER:
@@ -1307,6 +1402,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err) logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_gender_set)
res.Content = string(profileInfo) res.Content = string(profileInfo)
case common.DATA_YOB: case common.DATA_YOB:
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB)
@@ -1318,8 +1414,8 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err) logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_yob_set)
res.Content = string(profileInfo) res.Content = string(profileInfo)
case common.DATA_LOCATION: case common.DATA_LOCATION:
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
if err != nil { if err != nil {
@@ -1330,6 +1426,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err) logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_location_set)
res.Content = string(profileInfo) res.Content = string(profileInfo)
case common.DATA_OFFERINGS: case common.DATA_OFFERINGS:
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
@@ -1341,6 +1438,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err) logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err)
return res, err return res, err
} }
res.FlagSet = append(res.FlagSet, flag_offerings_set)
res.Content = string(profileInfo) res.Content = string(profileInfo)
default: default:
break break
@@ -1384,14 +1482,7 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
offerings := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)) offerings := getEntryOrDefault(store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS))
// Construct the full name // Construct the full name
name := defaultValue name := utils.ConstructName(firstName, familyName, defaultValue)
if familyName != defaultValue {
if firstName == defaultValue {
name = familyName
} else {
name = firstName + " " + familyName
}
}
// Calculate age from year of birth // Calculate age from year of birth
age := defaultValue age := defaultValue
@@ -1530,18 +1621,50 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// 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) data := common.ProcessVouchers(vouchersResp)
// Store all voucher data // Store all voucher data
dataMap := map[string]string{ dataMap := map[common.DataTyp]string{
"sym": data.Symbols, common.DATA_VOUCHER_SYMBOLS: data.Symbols,
"bal": data.Balances, common.DATA_VOUCHER_BALANCES: data.Balances,
"deci": data.Decimals, common.DATA_VOUCHER_DECIMALS: data.Decimals,
"addr": data.Addresses, common.DATA_VOUCHER_ADDRESSES: data.Addresses,
} }
for key, value := range dataMap { 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 return res, nil
} }
} }
@@ -1554,7 +1677,7 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
var res resource.Result var res resource.Result
// Read vouchers from the store // 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 { if err != nil {
logg.ErrorCtxf(ctx, "Failed to read the voucherData from prefixDb", "error", err) logg.ErrorCtxf(ctx, "Failed to read the voucherData from prefixDb", "error", err)
return res, err return res, err
@@ -1574,6 +1697,10 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
return res, fmt.Errorf("missing session") 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") flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
inputStr := string(input) inputStr := string(input)
@@ -1598,7 +1725,7 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
} }
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) 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 return res, nil
} }
@@ -1654,10 +1781,228 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
tokenSymbol := voucherData.TokenSymbol res.Content = fmt.Sprintf(
tokenName := voucherData.TokenName "Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", voucherData.TokenName, voucherData.TokenSymbol, voucherData.TokenCommodity, voucherData.TokenLocation,
)
res.Content = fmt.Sprintf("%s %s", tokenSymbol, tokenName)
return res, nil 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
}

View File

@@ -2,7 +2,6 @@ package ussd
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
"path" "path"
@@ -23,6 +22,7 @@ import (
testdataloader "github.com/peteole/testdata-loader" testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem" memdb "git.defalsify.org/vise.git/db/mem"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
@@ -57,7 +57,8 @@ func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPr
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
spdb := storage.NewSubPrefixDb(db, prefix)
return spdb return spdb
} }
@@ -180,11 +181,14 @@ func TestSaveFirstname(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_firstname_set, _ := fm.GetFlag("flag_firstname_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(128)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
firstName := "John" firstName := "John"
@@ -192,6 +196,8 @@ func TestSaveFirstname(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_firstname_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -204,7 +210,7 @@ func TestSaveFirstname(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_FIRST_NAME entry has been updated with the temporary value // Verify that the DATA_FIRST_NAME entry has been updated with the temporary value
storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
@@ -219,11 +225,16 @@ func TestSaveFamilyname(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_firstname_set, _ := fm.GetFlag("flag_familyname_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(128)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
expectedResult.FlagSet = []uint32{flag_firstname_set}
// Define test data // Define test data
familyName := "Doeee" familyName := "Doeee"
@@ -243,7 +254,7 @@ func TestSaveFamilyname(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value // Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value
storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
@@ -258,11 +269,14 @@ func TestSaveYoB(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_yob_set, _ := fm.GetFlag("flag_yob_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
yob := "1980" yob := "1980"
@@ -270,6 +284,8 @@ func TestSaveYoB(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_yob_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -282,7 +298,7 @@ func TestSaveYoB(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_YOB entry has been updated with the temporary value // Verify that the DATA_YOB entry has been updated with the temporary value
storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB) storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB)
@@ -297,11 +313,14 @@ func TestSaveLocation(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_location_set, _ := fm.GetFlag("flag_location_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
location := "Kilifi" location := "Kilifi"
@@ -309,6 +328,8 @@ func TestSaveLocation(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_location_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -321,7 +342,7 @@ func TestSaveLocation(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_LOCATION entry has been updated with the temporary value // Verify that the DATA_LOCATION entry has been updated with the temporary value
storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
@@ -336,11 +357,14 @@ func TestSaveOfferings(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_offerings_set, _ := fm.GetFlag("flag_offerings_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data // Define test data
offerings := "Bananas" offerings := "Bananas"
@@ -348,6 +372,8 @@ func TestSaveOfferings(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expectedResult.FlagSet = []uint32{flag_offerings_set}
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
@@ -360,7 +386,7 @@ func TestSaveOfferings(t *testing.T) {
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_OFFERINGS entry has been updated with the temporary value // Verify that the DATA_OFFERINGS entry has been updated with the temporary value
storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
@@ -375,9 +401,10 @@ func TestSaveGender(t *testing.T) {
fm, _ := NewFlagManager(flagsPath) fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update") flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_gender_set, _ := fm.GetFlag("flag_gender_set")
// Set the flag in the State // Set the flag in the State
mockState := state.NewState(16) mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update) mockState.SetFlag(flag_allow_update)
// Define test cases // Define test cases
@@ -421,12 +448,16 @@ func TestSaveGender(t *testing.T) {
flagManager: fm.parser, flagManager: fm.parser,
} }
expectedResult := resource.Result{}
// Call the method // Call the method
res, err := h.SaveGender(ctx, "save_gender", tt.input) res, err := h.SaveGender(ctx, "save_gender", tt.input)
expectedResult.FlagSet = []uint32{flag_gender_set}
// Assert results // Assert results
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res) assert.Equal(t, expectedResult, res)
// Verify that the DATA_GENDER entry has been updated with the temporary value // Verify that the DATA_GENDER entry has been updated with the temporary value
storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER) storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
@@ -1499,10 +1530,10 @@ func TestValidateRecipient(t *testing.T) {
}{ }{
{ {
name: "Test with invalid recepient", name: "Test with invalid recepient",
input: []byte("9234adf5"), input: []byte("7?1234"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_recipient}, FlagSet: []uint32{flag_invalid_recipient},
Content: "9234adf5", Content: "7?1234",
}, },
}, },
{ {
@@ -1518,22 +1549,40 @@ func TestValidateRecipient(t *testing.T) {
input: []byte("0711223344"), input: []byte("0711223344"),
expectedResult: resource.Result{}, 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 // store a public key for the valid recipient
err = store.WriteEntry(ctx, "0711223344", common.DATA_PUBLIC_KEY, []byte(publicKey)) err = store.WriteEntry(ctx, "+254711223344", common.DATA_PUBLIC_KEY, []byte(publicKey))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockAccountService := new(mocks.MockAccountService)
// Create the Handlers instance // Create the Handlers instance
h := &Handlers{ h := &Handlers{
flagManager: fm.parser, flagManager: fm.parser,
userdataStore: store, userdataStore: store,
accountService: mockAccountService,
} }
aliasResponse := &dataserviceapi.AliasAddress{
Address: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
mockAccountService.On("CheckAliasAddress", string(tt.input)).Return(aliasResponse, nil)
// Call the method // Call the method
res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input) res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input)
@@ -1565,7 +1614,7 @@ func TestCheckBalance(t *testing.T) {
publicKey: "0X98765432109", publicKey: "0X98765432109",
activeSym: "ETH", activeSym: "ETH",
activeBal: "1.5", activeBal: "1.5",
expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"}, expectedResult: resource.Result{Content: "Balance: 1.50 ETH\n"},
expectError: false, expectError: false,
}, },
} }
@@ -1773,58 +1822,43 @@ func TestConfirmPin(t *testing.T) {
} }
} }
func TestFetchCustodialBalances(t *testing.T) { func TestFetchCommunityBalance(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
// Define test data // Define test data
sessionId := "session123" sessionId := "session123"
publicKey := "0X13242618721"
ctx, store := InitializeTestStore(t) ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey))
if err != nil {
t.Fatal(err)
}
tests := []struct { tests := []struct {
name string name string
balanceResponse *models.BalanceResult languageCode string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test when fetch custodial balances is not a success", name: "Test community balance content when language is english",
balanceResponse: &models.BalanceResult{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagReset: []uint32{flag_api_error}, Content: "Community Balance: 0.00",
}, },
languageCode: "eng",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockAccountService := new(mocks.MockAccountService) mockAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16) mockState := state.NewState(16)
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
flagManager: fm.parser,
st: mockState, st: mockState,
accountService: mockAccountService, accountService: mockAccountService,
} }
ctx = context.WithValue(ctx, "SessionId", sessionId)
// Set up the expected behavior of the mock ctx = context.WithValue(ctx, "Language", lang.Language{
mockAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil) Code: tt.languageCode,
})
// Call the method // Call the method
res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte("")) res, _ := h.FetchCommunityBalance(ctx, "fetch_community_balance", []byte(""))
//Assert that the result set to content is what was expected //Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should match expected result") assert.Equal(t, res, tt.expectedResult, "Result should match expected result")
@@ -1935,7 +1969,7 @@ func TestCheckVouchers(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Read voucher sym data from the store // Read voucher sym data from the store
voucherData, err := spdb.Get(ctx, []byte("sym")) voucherData, err := spdb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1959,7 +1993,7 @@ func TestGetVoucherList(t *testing.T) {
expectedSym := []byte("1:SRF\n2:MILO") expectedSym := []byte("1:SRF\n2:MILO")
// Put voucher sym data from the store // Put voucher sym data from the store
err := spdb.Put(ctx, []byte("sym"), expectedSym) err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1989,16 +2023,16 @@ func TestViewVoucher(t *testing.T) {
} }
// Define mock voucher data // Define mock voucher data
mockData := map[string][]byte{ mockData := map[common.DataTyp][]byte{
"sym": []byte("1:SRF\n2:MILO"), common.DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"), common.DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"), common.DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), common.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
} }
// Put the data // Put the data
for key, value := range mockData { for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value)) err = spdb.Put(ctx, []byte(common.ToBytes(key)), []byte(value))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -2006,7 +2040,7 @@ func TestViewVoucher(t *testing.T) {
res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1")) res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1"))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, res.Content, "SRF\n100") assert.Equal(t, res.Content, "Symbol: SRF\nBalance: 100")
} }
func TestSetVoucher(t *testing.T) { func TestSetVoucher(t *testing.T) {
@@ -2040,3 +2074,42 @@ func TestSetVoucher(t *testing.T) {
assert.Equal(t, string(tempData.TokenSymbol), res.Content) assert.Equal(t, string(tempData.TokenSymbol), res.Content)
} }
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)
}

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
gdbmdb "git.defalsify.org/vise.git/db/gdbm" gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/lang"
) )
var ( var (
@@ -114,3 +114,8 @@ func(tdb *ThreadGdbmDb) Close() error {
tdb.db = nil tdb.db = nil
return err return err
} }
func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) {
logg.Warnf("method not implemented for thread gdbm db")
return nil, nil
}

View File

@@ -41,10 +41,13 @@ func buildConnStr() string {
dbName := initializers.GetEnv("DB_NAME", "") dbName := initializers.GetEnv("DB_NAME", "")
port := initializers.GetEnv("DB_PORT", "5432") port := initializers.GetEnv("DB_PORT", "5432")
return fmt.Sprintf( connString := fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s", "postgres://%s:%s@%s:%s/%s",
user, password, host, port, dbName, user, password, host, port, dbName,
) )
logg.Debugf("pg conn string", "conn", connString)
return connString
} }
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {

View File

@@ -6,10 +6,6 @@ import (
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
) )
const (
DATATYPE_USERSUB = 64
)
// PrefixDb interface abstracts the database operations. // PrefixDb interface abstracts the database operations.
type PrefixDb interface { type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error) Get(ctx context.Context, key []byte) ([]byte, error)
@@ -35,13 +31,13 @@ func (s *SubPrefixDb) toKey(k []byte) []byte {
} }
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { 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) key = s.toKey(key)
return s.store.Get(ctx, key) return s.store.Get(ctx, key)
} }
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { 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) key = s.toKey(key)
return s.store.Put(ctx, key, val) return s.store.Put(ctx, key, val)
} }

View File

@@ -47,3 +47,8 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to
args := m.Called() args := m.Called()
return args.Get(0).(*models.TokenTransferResponse), args.Error(1) 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)
}

View File

@@ -33,8 +33,8 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey
} }
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
return []dataserviceapi.TokenHoldings { return []dataserviceapi.TokenHoldings{
dataserviceapi.TokenHoldings { dataserviceapi.TokenHoldings{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF", TokenSymbol: "SRF",
TokenDecimals: "6", TokenDecimals: "6",
@@ -56,3 +56,7 @@ func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from,
TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
}, nil }, nil
} }
func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
return &dataserviceapi.AliasAddress{}, nil
}

View File

@@ -2,8 +2,8 @@
package testtag package testtag
import "git.grassecon.net/urdt/ussd/internal/handlers/server" import "git.grassecon.net/urdt/ussd/remote"
var ( var (
AccountService server.AccountServiceInterface AccountService remote.AccountServiceInterface
) )

View File

@@ -1,6 +1,9 @@
package utils 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 // 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. // 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. // It subtracts the YOB from the current year to determine the age.
// //
// Parameters: // Parameters:
// yob: The year of birth as an integer. //
// yob: The year of birth as an integer.
// //
// Returns: // Returns:
// The calculated age as an integer. //
// The calculated age as an integer.
func CalculateAgeWithYOB(yob int) int { func CalculateAgeWithYOB(yob int) int {
currentYear := time.Now().Year() currentYear := time.Now().Year()
return currentYear - yob 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
}
}

17
internal/utils/name.go Normal file
View File

@@ -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
}

View File

@@ -54,7 +54,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -62,7 +62,7 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Select language:\n0:english\n1:kiswahili" "expectedContent": "Select language:\n0:English\n1:Kiswahili"
}, },
{ {
"input": "0", "input": "0",
@@ -95,7 +95,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -103,7 +103,7 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit" "expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit"
}, },
{ {
"input": "0", "input": "0",
@@ -141,7 +141,7 @@
}, },
{ {
"input": "1235", "input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit" "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@@ -149,7 +149,7 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit" "expectedContent": "{balance}\n0:Back\n9:Quit"
}, },
{ {
"input": "0", "input": "0",
@@ -167,7 +167,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_firstname", "name": "menu_my_account_edit_all_account_details_starting_from_firstname",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -187,6 +187,26 @@
}, },
{ {
"input": "foo", "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:" "expectedContent": "Please enter your PIN:"
}, },
{ {
@@ -197,10 +217,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -208,7 +224,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_familyname", "name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -238,10 +254,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -250,7 +262,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_gender", "name": "menu_my_account_edit_gender_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -280,10 +292,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -291,7 +299,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_yob", "name": "menu_my_account_edit_yob_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -321,10 +329,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -332,7 +336,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_location", "name": "menu_my_account_edit_location_when_all_account_details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -362,10 +366,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -373,7 +373,7 @@
] ]
}, },
{ {
"name": "menu_my_account_edit_offerings", "name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@@ -403,10 +403,6 @@
"input": "0", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@@ -434,16 +430,12 @@
}, },
{ {
"input": "1234", "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", "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" "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", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"

View File

@@ -3,6 +3,7 @@ package menutraversaltest
import ( import (
"bytes" "bytes"
"context" "context"
"flag"
"log" "log"
"math/rand" "math/rand"
"os" "os"
@@ -15,14 +16,15 @@ import (
) )
var ( var (
testData = driver.ReadData() testData = driver.ReadData()
testStore = ".test_state" testStore = ".test_state"
groupTestFile = "group_test.json" sessionID string
sessionID string src = rand.NewSource(42)
src = rand.NewSource(42) g = rand.New(src)
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 { func GenerateSessionId() string {
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4() v, err := uu.NewV4()
@@ -337,7 +339,7 @@ func TestMainMenuSend(t *testing.T) {
} }
func TestGroups(t *testing.T) { func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile) groups, err := driver.LoadTestGroups(*groupTestFile)
if err != nil { if err != nil {
log.Fatalf("Failed to load test groups: %v", err) log.Fatalf("Failed to load test groups: %v", err)
} }

View File

@@ -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"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -7,11 +7,11 @@
"steps": [ "steps": [
{ {
"input": "", "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", "input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" "expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
}, },
{ {
"input": "0", "input": "0",
@@ -40,11 +40,11 @@
"steps": [ "steps": [
{ {
"input": "", "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", "input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" "expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
}, },
{ {
"input": "1", "input": "1",
@@ -61,7 +61,7 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back" "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
}, },
{ {
"input": "000", "input": "000",
@@ -69,7 +69,7 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back" "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
}, },
{ {
"input": "0712345678", "input": "0712345678",

18
models/profile.go Normal file
View File

@@ -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
}
}

View File

@@ -1,8 +1,10 @@
package models package models
type VoucherDataResult struct { type VoucherDataResult struct {
TokenName string `json:"tokenName"` TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"` TokenSymbol string `json:"tokenSymbol"`
TokenDecimals int `json:"tokenDecimals"` TokenDecimals int `json:"tokenDecimals"`
SinkAddress string `json:"sinkAddress"` SinkAddress string `json:"sinkAddress"`
TokenCommodity string `json:"tokenCommodity"`
TokenLocation string `json:"tokenLocation"`
} }

View File

@@ -24,6 +24,7 @@ type AccountServiceInterface interface {
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, 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 { type AccountService struct {
@@ -209,6 +210,26 @@ func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, t
return &r, nil 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) { func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
var okResponse api.OKResponse var okResponse api.OKResponse
var errResponse api.ErrResponse var errResponse api.ErrResponse

View File

@@ -0,0 +1 @@
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.

View File

@@ -0,0 +1 @@
Please enter your PIN to view statement:

View File

@@ -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 *

View File

@@ -0,0 +1 @@
Tafadhali weka PIN yako kuona taarifa ya matumizi:

View File

@@ -1 +1 @@
Salio la kikundi {{.fetch_community_balance}}

View File

@@ -1 +1 @@
{{.fetch_custodial_balances}} {{.fetch_community_balance}}

View File

@@ -1,7 +1,7 @@
LOAD reset_incorrect 6 LOAD reset_incorrect 6
LOAD fetch_custodial_balances 0 LOAD fetch_community_balance 0
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances MAP fetch_community_balance
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0 CATCH pin_entry flag_account_authorized 0
MOUT back 0 MOUT back 0

View File

@@ -2,9 +2,17 @@ CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1 CATCH update_familyname flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_familyname 0 RELOAD set_back
CATCH _ flag_back_set 1
LOAD save_familyname 64
RELOAD save_familyname RELOAD save_familyname
INCMP _ 0 CATCH pin_entry flag_familyname_set 1
INCMP pin_entry * 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 *

View File

@@ -5,7 +5,14 @@ RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_firstname 0 RELOAD set_back
CATCH _ flag_back_set 1
LOAD save_firstname 128
RELOAD save_firstname RELOAD save_firstname
INCMP _ 0 CATCH pin_entry flag_firstname_set 1
INCMP pin_entry * 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

View File

@@ -2,9 +2,14 @@ CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_location flag_allow_update 1 CATCH update_location flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
LOAD save_location 16
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_location 0 RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_location RELOAD save_location
INCMP _ 0 INCMP _ 0
INCMP pin_entry * CATCH pin_entry flag_location_set 1
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_location_set 0
INCMP edit_offerings *

View File

@@ -2,9 +2,13 @@ CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_offerings flag_allow_update 1 CATCH update_offerings flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
LOAD save_offerings 0 LOAD save_offerings 8
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_offerings RELOAD save_offerings
INCMP _ 0 INCMP _ 0
INCMP pin_entry * CATCH pin_entry flag_offerings_set 1
CATCH pin_entry flag_offerings_set 0
INCMP update_profile_items *

View File

@@ -11,7 +11,8 @@ MOUT edit_offerings 6
MOUT view 7 MOUT view 7
MOUT back 0 MOUT back 0
HALT HALT
INCMP my_account 0 LOAD set_back 6
INCMP ^ 0
INCMP edit_first_name 1 INCMP edit_first_name 1
INCMP edit_family_name 2 INCMP edit_family_name 2
INCMP select_gender 3 INCMP select_gender 3

View File

@@ -5,10 +5,15 @@ RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back
CATCH _ flag_back_set 1
LOAD verify_yob 6 LOAD verify_yob 6
RELOAD verify_yob RELOAD verify_yob
CATCH incorrect_date_format flag_incorrect_date_format 1 CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 0 LOAD save_yob 32
RELOAD save_yob RELOAD save_yob
INCMP _ 0 CATCH pin_entry flag_yob_set 1
INCMP pin_entry * CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_yob_set 0
INCMP edit_location *

View File

@@ -0,0 +1 @@
English

View File

@@ -1 +1 @@
Incorrect pin Incorrect PIN

View File

@@ -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.

View File

@@ -0,0 +1 @@
Kiswahili

View File

@@ -13,7 +13,7 @@ msgstr "Kwa usaidizi zaidi,piga: 0757628885"
msgid "Balance: %s\n" msgid "Balance: %s\n"
msgstr "Salio: %s\n" msgstr "Salio: %s\n"
msid "Your invite request for %s to Sarafu Network failed. Please try again later." 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." 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." msgid "Your invitation to %s to join Sarafu Network has been sent."
@@ -21,3 +21,9 @@ msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu limetumwa."
msgid "Your request failed. Please try again later." msgid "Your request failed. Please try again later."
msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye." 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"

View File

@@ -1,10 +1,10 @@
LOAD set_default_voucher 8 LOAD set_default_voucher 8
RELOAD set_default_voucher RELOAD set_default_voucher
LOAD check_balance 64
RELOAD check_balance
LOAD check_vouchers 10 LOAD check_vouchers 10
RELOAD check_vouchers 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 MAP check_balance
MOUT send 1 MOUT send 1
MOUT vouchers 2 MOUT vouchers 2

View File

@@ -11,5 +11,6 @@ INCMP main 0
INCMP edit_profile 1 INCMP edit_profile 1
INCMP change_language 2 INCMP change_language 2
INCMP balances 3 INCMP balances 3
INCMP check_statement 4
INCMP pin_management 5 INCMP pin_management 5
INCMP address 6 INCMP address 6

View File

@@ -1 +1 @@
{{.fetch_custodial_balances}} {{.check_balance}}

View File

@@ -1,7 +1,7 @@
LOAD reset_incorrect 6 LOAD reset_incorrect 6
LOAD fetch_custodial_balances 0 LOAD check_balance 0
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances MAP check_balance
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0 CATCH pin_entry flag_account_authorized 0
MOUT back 0 MOUT back 0

View File

@@ -1 +1 @@
Salio lako ni: 0.00 SRF {{.check_balance}}

View File

@@ -0,0 +1 @@
Next

View File

@@ -0,0 +1 @@
Mbele

View File

@@ -1 +1 @@
no No

View File

@@ -1 +1 @@
la La

View File

@@ -0,0 +1 @@
No transfers history

View File

@@ -0,0 +1,5 @@
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@@ -0,0 +1 @@
Hakuna historia kwa akaunti yako

View File

@@ -19,3 +19,12 @@ flag,flag_api_call_error,25,this is set when communication to an external servic
flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher 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_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_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
1 flag flag_language_set 8 checks whether the user has set their prefered language
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher
20 flag flag_admin_privilege 27 this is set when a user has admin privileges.
21 flag flag_unregistered_number 28 this is set when an unregistered phonenumber tries to perform an action
22 flag flag_no_transfers 29 this is set when a user does not have any transactions
23 flag flag_incorrect_statement 30 this is set when the selected statement is invalid
24 flag flag_firstname_set 31 this is set when the first name of the profile is set
25 flag flag_familyname_set 32 this is set when the family name of the profile is set
26 flag flag_yob_set 33 this is set when the yob of the profile is set
27 flag flag_gender_set 34 this is set when the gender of the profile is set
28 flag flag_location_set 35 this is set when the location of the profile is set
29 flag flag_offerings_set 36 this is set when the offerings of the profile is set
30 flag flag_back_set 37 this is set when it is a back navigation

View File

@@ -0,0 +1 @@
Prev

View File

@@ -0,0 +1 @@
Nyuma

View File

@@ -1,3 +1,5 @@
LOAD update_all_profile_items 0
RELOAD update_all_profile_items
MOUT back 0 MOUT back 0
MOUT quit 9 MOUT quit 9
HALT HALT

View File

@@ -1 +1 @@
Enter recipient's phone number: Enter recipient's phone number/address/alias:

View File

@@ -1,3 +1,4 @@
LOAD set_language 6 LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
MOVE language_changed MOVE language_changed

View File

@@ -1,4 +1,10 @@
LOAD save_gender 0 LOAD save_gender 32
RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 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

View File

@@ -1,4 +1,10 @@
LOAD save_gender 0 LOAD save_gender 16
RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 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

View File

@@ -1,3 +1,4 @@
LOAD set_language 6 LOAD set_language 6
RELOAD set_language
CATCH terms flag_account_created 0 CATCH terms flag_account_created 0
MOVE language_changed MOVE language_changed

View File

@@ -1,4 +1,10 @@
LOAD save_gender 0 LOAD save_gender 8
RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 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

View File

@@ -0,0 +1 @@
{{.get_transactions}}

View File

@@ -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 *

View File

@@ -0,0 +1 @@
{{.get_transactions}}

View File

@@ -0,0 +1,3 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
MOVE pin_entry

View File

@@ -0,0 +1 @@
{{.view_statement}}

View File

@@ -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

View File

@@ -1 +1 @@
yes Yes

View File

@@ -1 +1 @@
ndio Ndio