menu-voucherlist #101

Merged
lash merged 63 commits from menu-voucherlist into master 2024-10-25 15:59:47 +02:00
37 changed files with 487 additions and 324 deletions
Showing only changes of commit b3c7a3a337 - Show all commits

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ go.work*
**/*/*.bin **/*/*.bin
**/*/.state/ **/*/.state/
cmd/.state/ cmd/.state/
id_*
*.gdbm

2
go.mod
View File

@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
go 1.22.6 go 1.22.6
require ( require (
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb git.defalsify.org/vise.git v0.1.0-rc.3.0.20240926120105-89b0529cf7ac
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/peteole/testdata-loader v0.3.0 github.com/peteole/testdata-loader v0.3.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1

2
go.sum
View File

@ -1,5 +1,7 @@
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y= git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240926120105-89b0529cf7ac h1:D4KI22KWXT8S66sHIjWhTBX6SXRfnd7j8VErq3PPbok=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240926120105-89b0529cf7ac/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
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=

View File

@ -60,8 +60,8 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) {
ussdHandlers = ussdHandlers.WithPersister(ls.Pe) ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
@ -88,13 +88,13 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) {
ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("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("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
return ussdHandlers, nil return ussdHandlers, nil
} }

View File

@ -14,6 +14,7 @@ type AccountServiceInterface interface {
CheckBalance(publicKey string) (string, error) CheckBalance(publicKey string) (string, error)
CreateAccount() (*models.AccountResponse, error) CreateAccount() (*models.AccountResponse, error)
CheckAccountStatus(trackingId string) (string, error) CheckAccountStatus(trackingId string) (string, error)
FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error)
} }
type AccountService struct { type AccountService struct {
@ -106,6 +107,54 @@ func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
return &accountResp, nil return &accountResp, nil
} }
// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error) {
Outdated
Review

Is this in use? Looks like test code?

Is this in use? Looks like test code?
// TODO replace with the actual request once ready
mockJSON := `{
"ok": true,
"description": "Token holdings with current balances",
"result": {
"holdings": [
{
"contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
"tokenSymbol": "FSPTST",
"tokenDecimals": "6",
"balance": "8869964242"
},
{
"contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA",
"tokenSymbol": "GEO",
"tokenDecimals": "6",
"balance": "9884"
},
{
"contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273",
"tokenSymbol": "MFNK",
"tokenDecimals": "6",
"balance": "19788697"
},
{
"contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83",
"tokenSymbol": "MILO",
"tokenDecimals": "6",
"balance": "75"
}
]
}
}`
// Unmarshal the JSON response
var holdings models.VoucherHoldingResponse
err := json.Unmarshal([]byte(mockJSON), &holdings)
if err != nil {
return nil, err
}
return &holdings, nil
}
func GetTokenList() (*models.ApiResponse, error) { func GetTokenList() (*models.ApiResponse, error) {
file, err := os.Open("sample_tokens.json") file, err := os.Open("sample_tokens.json")
if err != nil { if err != nil {

View File

@ -21,6 +21,8 @@ import (
"git.grassecon.net/urdt/ussd/internal/handlers/server" "git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/utils" "git.grassecon.net/urdt/ussd/internal/utils"
"gopkg.in/leonelquinteros/gotext.v1" "gopkg.in/leonelquinteros/gotext.v1"
"git.grassecon.net/urdt/ussd/internal/storage"
) )
var ( var (
@ -117,17 +119,14 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
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
sym, _ = h.st.Where() symbol, _ := h.st.Where()
code := strings.Split(symbol, "_")[1]
switch sym { if !utils.IsValidISO639(code) {
case "set_default": return res, nil
res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
res.Content = "eng"
case "set_swa":
res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
res.Content = "swa"
default:
} }
res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
res.Content = code
languageSetFlag, err := h.flagManager.GetFlag("flag_language_set") languageSetFlag, err := h.flagManager.GetFlag("flag_language_set")
if err != nil { if err != nil {
@ -183,34 +182,6 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// SavePin persists the user's PIN choice into the filesystem
func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
// Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) {
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{} res := resource.Result{}
_, ok := ctx.Value("SessionId").(string) _, ok := ctx.Value("SessionId").(string)
@ -229,6 +200,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_PIN
// during the account creation process
// and during the change PIN process
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
@ -237,6 +211,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input) accountPIN := string(input)
@ -246,61 +221,36 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
res.FlagSet = append(res.FlagSet, flag_incorrect_pin) res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil return res, nil
} }
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN)) err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN))
if err != nil { if err != nil {
return res, err return res, err
} }
return res, nil return res, nil
} }
// GetVoucherList fetches the list of vouchers and formats them
// checks whether they are stored internally before calling the API
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{} var res resource.Result
//as := h.accountService.(*server.AccountService)
tokenList,err := server.GetTokenList() // check if the vouchers exist internally and if not
fmt.Println("Error here:",err) // fetch from the API
// Read vouchers from the store
store := h.userdataStore
prefixdb := storage.NewSubPrefixDb(store, []byte("token_holdings"))
voucherData, err := prefixdb.Get(ctx, []byte("tokens"))
if err != nil { if err != nil {
return res,err return res, nil
} }
holdings := tokenList.Result.Holdings res.Content = string(voucherData)
fmt.Println("TokenList:",tokenList.Result.Holdings)
// vouchers := []string{
// "SRF",
// "CRF",
// "VCF",
// "VSAPA",
// "FSTMP",
// "FSAW",
// "PTAQ",
// "VCRXT",
// "VSGAQ",
// "QPWIQQ",
// "FSTMP",
// "FSAW",
// "PTAQ",
// "VCRXT",
// "VSGAQ",
// "QPWIQQ",
// "FSTMP",
// "FSAW",
// "PTAQ",
// "VCRXT",
// "VSGAQ",
// "QPWIQQ",
// }
var numberedVouchers []string
for i,token := range holdings {
numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, token.TokenSymbol))
}
// var numberedVouchers []string
// for i, voucher := range vouchers {
// numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher))
// }
res.Content = strings.Join(numberedVouchers,"\n")
return res, nil return res, nil
} }
@ -330,36 +280,10 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
return res, nil return res, nil
} }
// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information. // VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) { // If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
var res resource.Result
menuOption := string(input)
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_single_edit, _ := h.flagManager.GetFlag("flag_single_edit")
switch menuOption {
case "2":
res.FlagReset = append(res.FlagReset, flag_allow_update)
res.FlagSet = append(res.FlagSet, flag_single_edit)
case "3":
res.FlagReset = append(res.FlagReset, flag_allow_update)
res.FlagSet = append(res.FlagSet, flag_single_edit)
case "4":
res.FlagReset = append(res.FlagReset, flag_allow_update)
res.FlagSet = append(res.FlagSet, flag_single_edit)
default:
res.FlagReset = append(res.FlagReset, flag_single_edit)
}
return res, nil
}
// VerifyPin checks whether the confirmation PIN is similar to the account PIN
// If similar, it sets the USERFLAG_PIN_SET flag allowing the user
// to access the main menu // to access the main menu
func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin") flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
@ -371,14 +295,13 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
//AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN)
store := h.userdataStore store := h.userdataStore
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN) temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN)
if err != nil { if err != nil {
return res, err return res, err
} }
if bytes.Equal(input, AccountPin) { if bytes.Equal(input, temporaryPin) {
res.FlagSet = []uint32{flag_valid_pin} res.FlagSet = []uint32{flag_valid_pin}
res.FlagReset = []uint32{flag_pin_mismatch} res.FlagReset = []uint32{flag_pin_mismatch}
res.FlagSet = append(res.FlagSet, flag_pin_set) res.FlagSet = append(res.FlagSet, flag_pin_set)
@ -386,6 +309,11 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
res.FlagSet = []uint32{flag_pin_mismatch} res.FlagSet = []uint32{flag_pin_mismatch}
} }
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin))
if err != nil {
return res, err
}
return res, nil return res, nil
} }
@ -486,6 +414,7 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (
// SaveGender updates the gender in the gdbm with the provided input. // SaveGender updates the gender in the gdbm with the provided input.
func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
symbol, _ := h.st.Where()
var res resource.Result var res resource.Result
var err error var err error
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
@ -493,22 +422,12 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) > 0 { gender := strings.Split(symbol, "_")[1]
gender := string(input)
switch gender {
case "1":
gender = "Male"
case "2":
gender = "Female"
case "3":
gender = "Unspecified"
}
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
if err != nil { if err != nil {
return res, nil return res, nil
} }
}
return res, nil return res, nil
} }
@ -1089,3 +1008,116 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
Outdated
Review

s/gdbm/db/

s/gdbm/db/
return res, nil return res, nil
} }
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
// them to gdbm
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(string(publicKey))
if err != nil {
return res, nil
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Can we put this in a separate method please (that can be unit-tested with data only)

Can we put this in a separate method please (that can be unit-tested with data only)
}
var numberedSymbols []string
var numberedBalances []string
for i, voucher := range vouchersResp.Result.Holdings {
numberedSymbols = append(numberedSymbols, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol))
numberedBalances = append(numberedBalances, fmt.Sprintf("%d:%s", i+1, voucher.Balance))
}
voucherSymbolList := strings.Join(numberedSymbols, "\n")
voucherBalanceList := strings.Join(numberedBalances, "\n")
prefixdb := storage.NewSubPrefixDb(store, []byte("token_holdings"))
err = prefixdb.Put(ctx, []byte("tokens"), []byte(voucherSymbolList))
if err != nil {
return res, nil
}
err = prefixdb.Put(ctx, []byte(voucherSymbolList), []byte(voucherBalanceList))
if err != nil {
return res, nil
}
return res, nil
}
// ViewVoucher retrieves the token holding and balance from the subprefixDB
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
inputStr := string(input)
if inputStr == "0" || inputStr == "00" {
return res, nil
}
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
// Initialize the store and prefix database
store := h.userdataStore
prefixdb := storage.NewSubPrefixDb(store, []byte("token_holdings"))
// Retrieve the voucher symbol list
voucherSymbolList, err := prefixdb.Get(ctx, []byte("tokens"))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher symbol list: %v", err)
}
// Retrieve the voucher balance list
voucherBalanceList, err := prefixdb.Get(ctx, []byte(voucherSymbolList))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err)
}
// Convert the symbol and balance lists from byte arrays to strings
lash marked this conversation as resolved Outdated
Outdated
Review

Now this is code smell; we don't want to use menu selectors in branching here.

But I assume it is done because it is necessary to determine whether we are selecting token or merely navigating laterally? If that is the case, I will make an issue for go-vise to provide whether lateral is indeed the case.

Now this is code smell; we don't want to use menu selectors in branching here. But I assume it is done because it is necessary to determine whether we are selecting token or merely navigating laterally? If that is the case, I will make an issue for go-vise to provide whether lateral is indeed the case.

This was added because the vise runs the function when we're navigating, instead of prioritizing the INCMP and navigate to the specified node

This was added because the vise runs the function when we're navigating, instead of prioritizing the INCMP and navigate to the specified node
voucherSymbols := string(voucherSymbolList)
voucherBalances := string(voucherBalanceList)
// Split the lists into slices for processing
symbols := strings.Split(voucherSymbols, "\n")
balances := strings.Split(voucherBalances, "\n")
var matchedSymbol, matchedBalance string
for i, symbol := range symbols {
symbolParts := strings.SplitN(symbol, ":", 2)
if len(symbolParts) != 2 {
continue
}
voucherNum := symbolParts[0]
voucherSymbol := symbolParts[1]
// Check if input matches either the number or the symbol
if inputStr == voucherNum || strings.EqualFold(inputStr, voucherSymbol) {
matchedSymbol = voucherSymbol
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Please put in separate, unit-testable method.

Please put in separate, unit-testable method.
// Ensure there's a corresponding balance
if i < len(balances) {
matchedBalance = strings.SplitN(balances[i], ":", 2)[1] // Extract balance after the "x:balance" format
}
break
}
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Not a very descriptive prefix?

Not a very descriptive prefix?
}
// If a match is found, return the symbol and balance
if matchedSymbol != "" && matchedBalance != "" {
res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance)
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
} else {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
}
return res, nil
}

View File

@ -9,6 +9,7 @@ import (
"testing" "testing"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state" "git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/mocks" "git.grassecon.net/urdt/ussd/internal/mocks"
@ -16,6 +17,7 @@ import (
"git.grassecon.net/urdt/ussd/internal/utils" "git.grassecon.net/urdt/ussd/internal/utils"
"github.com/alecthomas/assert/v2" "github.com/alecthomas/assert/v2"
testdataloader "github.com/peteole/testdata-loader" testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require"
) )
var ( var (
@ -94,6 +96,25 @@ func TestCreateAccount(t *testing.T) {
mockDataStore.AssertExpectations(t) mockDataStore.AssertExpectations(t)
} }
func TestWithPersister(t *testing.T) {
// Test case: Setting a persister
h := &Handlers{}
p := &persist.Persister{}
result := h.WithPersister(p)
assert.Equal(t, p, h.pe, "The persister should be set correctly.")
assert.Equal(t, h, result, "The returned handler should be the same instance.")
}
func TestWithPersister_PanicWhenAlreadySet(t *testing.T) {
// Test case: Panic on multiple calls
h := &Handlers{pe: &persist.Persister{}}
require.Panics(t, func() {
h.WithPersister(&persist.Persister{})
}, "Should panic when trying to set a persister again.")
}
func TestSaveFirstname(t *testing.T) { func TestSaveFirstname(t *testing.T) {
// Create a new instance of MockMyDataStore // Create a new instance of MockMyDataStore
mockStore := new(mocks.MockUserDataStore) mockStore := new(mocks.MockUserDataStore)
@ -150,7 +171,7 @@ func TestSaveFamilyname(t *testing.T) {
mockStore.AssertExpectations(t) mockStore.AssertExpectations(t)
} }
func TestSavePin(t *testing.T) { func TestSaveTemporaryPIn(t *testing.T) {
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

CAps

CAps
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
mockStore := new(mocks.MockUserDataStore) mockStore := new(mocks.MockUserDataStore)
if err != nil { if err != nil {
@ -192,10 +213,10 @@ func TestSavePin(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Set up the expected behavior of the mock // Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.input)).Return(nil) mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(tt.input)).Return(nil)
// Call the method // Call the method
res, err := h.SavePin(ctx, "save_pin", tt.input) res, err := h.SaveTemporaryPin(ctx, "save_pin", tt.input)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -295,6 +316,7 @@ func TestSaveOfferings(t *testing.T) {
func TestSaveGender(t *testing.T) { func TestSaveGender(t *testing.T) {
// Create a new instance of MockMyDataStore // Create a new instance of MockMyDataStore
mockStore := new(mocks.MockUserDataStore) mockStore := new(mocks.MockUserDataStore)
mockState := state.NewState(16)
// Define the session ID and context // Define the session ID and context
sessionId := "session123" sessionId := "session123"
@ -306,31 +328,29 @@ func TestSaveGender(t *testing.T) {
input []byte input []byte
expectedGender string expectedGender string
expectCall bool expectCall bool
executingSymbol string
}{ }{
{ {
name: "Valid Male Input", name: "Valid Male Input",
input: []byte("1"), input: []byte("1"),
expectedGender: "Male", expectedGender: "male",
executingSymbol: "set_male",
expectCall: true, expectCall: true,
}, },
{ {
name: "Valid Female Input", name: "Valid Female Input",
input: []byte("2"), input: []byte("2"),
expectedGender: "Female", expectedGender: "female",
executingSymbol: "set_female",
expectCall: true, expectCall: true,
}, },
{ {
name: "Valid Unspecified Input", name: "Valid Unspecified Input",
input: []byte("3"), input: []byte("3"),
expectedGender: "Unspecified", executingSymbol: "set_unspecified",
expectedGender: "unspecified",
expectCall: true, expectCall: true,
}, },
{
name: "Empty Input",
input: []byte(""),
expectedGender: "",
expectCall: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
@ -342,14 +362,15 @@ func TestSaveGender(t *testing.T) {
} else { } else {
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_GENDER, []byte(tt.expectedGender)).Return(nil) mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_GENDER, []byte(tt.expectedGender)).Return(nil)
} }
mockState.ExecPath = append(mockState.ExecPath, tt.executingSymbol)
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: mockStore, userdataStore: mockStore,
st: mockState,
} }
// Call the method // Call the method
_, err := h.SaveGender(ctx, "someSym", tt.input) _, err := h.SaveGender(ctx, "save_gender", tt.input)
// Assert no error // Assert no error
assert.NoError(t, err) assert.NoError(t, err)
@ -544,7 +565,7 @@ func TestSetLanguage(t *testing.T) {
}{ }{
{ {
name: "Set Default Language (English)", name: "Set Default Language (English)",
execPath: []string{"set_default"}, execPath: []string{"set_eng"},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{state.FLAG_LANG, 8}, FlagSet: []uint32{state.FLAG_LANG, 8},
Content: "eng", Content: "eng",
@ -558,13 +579,6 @@ func TestSetLanguage(t *testing.T) {
Content: "swa", Content: "swa",
}, },
}, },
{
name: "Unhandled path",
execPath: []string{""},
expectedResult: resource.Result{
FlagSet: []uint32{8},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
@ -592,76 +606,6 @@ func TestSetLanguage(t *testing.T) {
}) })
} }
} }
func TestSetResetSingleEdit(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update")
flag_single_edit, _ := fm.parser.GetFlag("flag_single_edit")
if err != nil {
log.Fatal(err)
}
// Define test cases
tests := []struct {
name string
input []byte
expectedResult resource.Result
}{
{
name: "Set single Edit",
input: []byte("2"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_single_edit},
FlagReset: []uint32{flag_allow_update},
},
},
{
name: "Set single Edit",
input: []byte("3"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_single_edit},
FlagReset: []uint32{flag_allow_update},
},
},
{
name: "Set single edit",
input: []byte("4"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_allow_update},
FlagSet: []uint32{flag_single_edit},
},
},
{
name: "No single edit set",
input: []byte("1"),
expectedResult: resource.Result{
FlagReset: []uint32{flag_single_edit},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create the Handlers instance with the mock flag manager
h := &Handlers{
flagManager: fm.parser,
}
// Call the method
res, err := h.SetResetSingleEdit(context.Background(), "set_reset_single_edit", tt.input)
if err != nil {
t.Error(err)
}
// Assert that the Result FlagSet has the required flags after language switch
assert.Equal(t, res, tt.expectedResult, "Flags should match reset edit")
})
}
}
func TestResetAllowUpdate(t *testing.T) { func TestResetAllowUpdate(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
@ -993,7 +937,7 @@ func TestVerifyYob(t *testing.T) {
} }
} }
func TestVerifyPin(t *testing.T) { func TestVerifyCreatePin(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
@ -1042,7 +986,7 @@ func TestVerifyPin(t *testing.T) {
}, },
} }
typ := utils.DATA_ACCOUNT_PIN typ := utils.DATA_TEMPORARY_PIN
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -1050,8 +994,11 @@ func TestVerifyPin(t *testing.T) {
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil) mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil)
// Set up the expected behavior of the mock
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(firstSetPin)).Return(nil)
// Call the method under test // Call the method under test
res, err := h.VerifyPin(ctx, "verify_pin", []byte(tt.input)) res, err := h.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input))
// Assert that no errors occurred // Assert that no errors occurred
assert.NoError(t, err) assert.NoError(t, err)
@ -1480,7 +1427,7 @@ func TestValidateAmount(t *testing.T) {
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
//flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount")
mockDataStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService) mockCreateAccountService := new(mocks.MockAccountService)
@ -1509,26 +1456,26 @@ func TestValidateAmount(t *testing.T) {
Content: "0.001", Content: "0.001",
}, },
}, },
// { {
// name: "Test with amount larger than balance", name: "Test with amount larger than balance",
// input: []byte("0.02"), input: []byte("0.02"),
// balance: "0.003 CELO", balance: "0.003 CELO",
// publicKey: []byte("0xrqeqrequuq"), publicKey: []byte("0xrqeqrequuq"),
// expectedResult: resource.Result{ expectedResult: resource.Result{
// FlagSet: []uint32{flag_invalid_amount}, FlagSet: []uint32{flag_invalid_amount},
// Content: "0.02", Content: "0.02",
// }, },
// }, },
// { {
// name: "Test with invalid amount", name: "Test with invalid amount",
// input: []byte("0.02ms"), input: []byte("0.02ms"),
// balance: "0.003 CELO", balance: "0.003 CELO",
// publicKey: []byte("0xrqeqrequuq"), publicKey: []byte("0xrqeqrequuq"),
// expectedResult: resource.Result{ expectedResult: resource.Result{
// FlagSet: []uint32{flag_invalid_amount}, FlagSet: []uint32{flag_invalid_amount},
// Content: "0.02ms", Content: "0.02ms",
// }, },
// }, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -1536,7 +1483,7 @@ func TestValidateAmount(t *testing.T) {
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil)
mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil) mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil) mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe()
// Call the method under test // Call the method under test
res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input) res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input)
@ -1648,6 +1595,7 @@ func TestGetProfile(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService) mockCreateAccountService := new(mocks.MockAccountService)
h := &Handlers{ h := &Handlers{
userdataStore: mockDataStore, userdataStore: mockDataStore,
accountService: mockCreateAccountService, accountService: mockCreateAccountService,
@ -1748,42 +1696,6 @@ func TestVerifyNewPin(t *testing.T) {
} }
func TestSaveTemporaryPIn(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
// Create a new instance of UserDataStore
mockStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
PIN := "1234"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
flagManager: fm.parser,
}
// Call the method
res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN))
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
// Assert all expectations were met
mockStore.AssertExpectations(t)
}
func TestConfirmPin(t *testing.T) { func TestConfirmPin(t *testing.T) {
sessionId := "session123" sessionId := "session123"

View File

@ -24,3 +24,8 @@ func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, erro
args := m.Called(trackingId) args := m.Called(trackingId)
return args.String(0), args.Error(1) return args.String(0), args.Error(1)
} }
func (m *MockAccountService) FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error) {
args := m.Called()
return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1)
}

View File

@ -0,0 +1,15 @@
package models
// VoucherHoldingResponse represents a single voucher holding
type VoucherHoldingResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
} `json:"result"`
}

43
internal/storage/db.go Normal file
View File

@ -0,0 +1,43 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
)
const (
DATATYPE_USERSUB = 64
)
type SubPrefixDb struct {
store db.Db
pfx []byte
}
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{
store: store,
pfx: pfx,
}
}
func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
v, err := s.store.Get(ctx, key)
if err != nil {
return nil, err
}
return v, nil
}
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
}

View File

@ -0,0 +1,54 @@
package storage
import (
"bytes"
"context"
"testing"
memdb "git.defalsify.org/vise.git/db/mem"
)
func TestSubPrefix(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
if err != nil {
t.Fatal(err)
}
r, err := sdba.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
sdbb := NewSubPrefixDb(db, []byte("lala"))
r, err = sdbb.Get(ctx, []byte("foo"))
if err == nil {
t.Fatal("expected not found")
}
err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
if err != nil {
t.Fatal(err)
}
r, err = sdbb.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("pu")) {
t.Fatalf("expected 'pu', got %s", r)
}
r, err = sdba.Get(ctx, []byte("foo"))
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
}

View File

@ -5,10 +5,6 @@ import (
"git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/persist"
) )
const (
DATATYPE_CUSTOM = 128
)
type Storage struct { type Storage struct {
Persister *persist.Persister Persister *persist.Persister
UserdataDb db.Db UserdataDb db.Db

View File

@ -23,6 +23,7 @@ const (
DATA_RECIPIENT DATA_RECIPIENT
DATA_AMOUNT DATA_AMOUNT
DATA_TEMPORARY_PIN DATA_TEMPORARY_PIN
DATA_VOUCHER_LIST
) )
func typToBytes(typ DataTyp) []byte { func typToBytes(typ DataTyp) []byte {

11
internal/utils/isocode.go Normal file
View File

@ -0,0 +1,11 @@
package utils
var isoCodes = map[string]bool{
"eng": true, // English
"swa": true, // Swahili
}
func IsValidISO639(code string) bool {
return isoCodes[code]
}

View File

@ -0,0 +1 @@
Something went wrong.Please try again

View File

@ -0,0 +1 @@
HALT

View File

@ -1,4 +1,4 @@
RELOAD verify_pin RELOAD verify_create_pin
CATCH create_pin_mismatch flag_pin_mismatch 1 CATCH create_pin_mismatch flag_pin_mismatch 1
LOAD quit 0 LOAD quit 0
HALT HALT

View File

@ -1,4 +1,4 @@
LOAD save_pin 0 LOAD save_temporary_pin 0
HALT HALT
LOAD verify_pin 8 LOAD verify_create_pin 8
INCMP account_creation * INCMP account_creation *

View File

@ -2,8 +2,8 @@ LOAD create_account 0
CATCH account_creation_failed flag_account_creation_failed 1 CATCH account_creation_failed flag_account_creation_failed 1
MOUT exit 0 MOUT exit 0
HALT HALT
LOAD save_pin 0 LOAD save_temporary_pin 0
RELOAD save_pin RELOAD save_temporary_pin
CATCH . flag_incorrect_pin 1 CATCH . flag_incorrect_pin 1
INCMP quit 0 INCMP quit 0
INCMP confirm_create_pin * INCMP confirm_create_pin *

View File

@ -11,8 +11,6 @@ MOUT view 7
MOUT back 0 MOUT back 0
HALT HALT
INCMP my_account 0 INCMP my_account 0
LOAD set_reset_single_edit 0
RELOAD set_reset_single_edit
INCMP enter_name 1 INCMP enter_name 1
INCMP enter_familyname 2 INCMP enter_familyname 2
INCMP select_gender 3 INCMP select_gender 3

View File

@ -1,5 +1,7 @@
LOAD check_balance 64 LOAD check_balance 64
RELOAD check_balance RELOAD check_balance
LOAD check_vouchers 10
RELOAD check_vouchers
MAP check_balance MAP check_balance
MOUT send 1 MOUT send 1
MOUT vouchers 2 MOUT vouchers 2

View File

@ -4,6 +4,3 @@ MOUT back 0
HALT HALT
INCMP _ 0 INCMP _ 0
INCMP select_voucher 1 INCMP select_voucher 1

View File

@ -12,5 +12,5 @@ flag,flag_invalid_amount,18,this is set when the given transaction amount is inv
flag,flag_incorrect_pin,19,this is set when the provided PIN is invalid or does not match the current account's PIN flag,flag_incorrect_pin,19,this is set when the provided PIN is invalid or does not match the current account's PIN
flag,flag_valid_pin,20,this is set when the given PIN is valid flag,flag_valid_pin,20,this is set when the given PIN is valid
flag,flag_allow_update,21,this is set to allow a user to update their profile data flag,flag_allow_update,21,this is set to allow a user to update their profile data
flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth flag,flag_incorrect_voucher,22,this is set when the selected voucher is invalid
flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid

1 flag flag_language_set 8 checks whether the user has set their prefered language
12 flag flag_incorrect_pin 19 this is set when the provided PIN is invalid or does not match the current account's PIN
13 flag flag_valid_pin 20 this is set when the given PIN is valid
14 flag flag_allow_update 21 this is set to allow a user to update their profile data
15 flag flag_single_edit flag_incorrect_voucher 22 this is set to allow a user to edit a single profile item such as year of birth this is set when the selected voucher is invalid
16 flag flag_incorrect_date_format 23 this is set when the given year of birth is invalid

View File

@ -1,13 +1,15 @@
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1 CATCH profile_update_success flag_allow_update 1
LOAD save_gender 0
MOUT male 1 MOUT male 1
MOUT female 2 MOUT female 2
MOUT unspecified 3 MOUT unspecified 3
MOUT back 0 MOUT back 0
HALT HALT
RELOAD save_gender
INCMP _ 0 INCMP _ 0
INCMP pin_entry * INCMP set_male 1
INCMP set_female 2
INCMP set_unspecified 3

View File

@ -1,6 +1,6 @@
MOUT english 0 MOUT english 0
MOUT kiswahili 1 MOUT kiswahili 1
HALT HALT
INCMP set_default 0 INCMP set_eng 0
INCMP set_swa 1 INCMP set_swa 1
INCMP . * INCMP . *

View File

@ -1,11 +1,15 @@
LOAD get_vouchers 0 LOAD get_vouchers 0
MAP get_vouchers MAP get_vouchers
MOUT back 0 MOUT back 0
MOUT quit 9 MOUT quit 00
MNEXT next 11 MNEXT next 11
MPREV prev 22 MPREV prev 22
HALT HALT
LOAD view_voucher 80
RELOAD view_voucher
CATCH . flag_incorrect_voucher 1
INCMP _ 0 INCMP _ 0
INCMP quit 9 INCMP quit 9
INCMP > 11 INCMP > 11
INCMP < 22 INCMP < 22
INCMP view_voucher *

View File

@ -0,0 +1,2 @@
Chagua nambari au ishara kutoka kwa salio zako:
{{.get_vouchers}}

View File

@ -0,0 +1,4 @@
LOAD save_gender 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
MOVE pin_entry

View File

@ -0,0 +1,4 @@
LOAD save_gender 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
MOVE pin_entry

View File

@ -0,0 +1,4 @@
LOAD save_gender 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
MOVE pin_entry

View File

@ -0,0 +1,2 @@
Enter PIN to confirm selection:
{{.view_voucher}}

View File

@ -0,0 +1,11 @@
RELOAD view_voucher
MAP view_voucher
MOUT back 0
MOUT quit 9
LOAD authorize_account 6
HALT
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP voucher_set *

View File

@ -0,0 +1,2 @@
Weka PIN ili kuthibitisha chaguo:
{{.view_voucher}}

View File

@ -0,0 +1 @@
Success! symbol is now your active voucher.

View File

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

View File

@ -0,0 +1 @@
Hongera! symbol ni Sarafu inayotumika sasa.