Merge remote-tracking branch 'refs/remotes/origin/menu-voucherlist' into menu-voucherlist

This commit is contained in:
Carlosokumu 2024-10-08 13:43:19 +03:00
commit b3c7a3a337
Signed by untrusted user: carlos
GPG Key ID: 7BD6BC8160A5C953
37 changed files with 487 additions and 324 deletions

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("get_vouchers",ussdHandlers.GetVoucherList) ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
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) {
// 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,63 +221,38 @@ 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) {
var res resource.Result
func (h *Handlers) GetVoucherList(ctx context.Context,sym string,input []byte) (resource.Result,error){ // check if the vouchers exist internally and if not
res := resource.Result{} // fetch from the API
//as := h.accountService.(*server.AccountService)
tokenList,err := server.GetTokenList() // Read vouchers from the store
fmt.Println("Error here:",err) 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 return res, nil
// for i, voucher := range vouchers {
// numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher))
// }
res.Content = strings.Join(numberedVouchers,"\n")
return res,nil
} }
func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) {
@ -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,21 +422,11 @@ 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) store := h.userdataStore
switch gender { err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
case "1": if err != nil {
gender = "Male" return res, nil
case "2":
gender = "Female"
case "3":
gender = "Unspecified"
}
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
if err != 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)
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
}
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
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
// 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
}
}
// 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) {
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"
@ -302,34 +324,32 @@ func TestSaveGender(t *testing.T) {
// Define test cases // Define test cases
tests := []struct { tests := []struct {
name string name string
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",
expectCall: true, executingSymbol: "set_male",
expectCall: true,
}, },
{ {
name: "Valid Female Input", name: "Valid Female Input",
input: []byte("2"), input: []byte("2"),
expectedGender: "Female", expectedGender: "female",
expectCall: true, executingSymbol: "set_female",
expectCall: true,
}, },
{ {
name: "Valid Unspecified Input", name: "Valid Unspecified Input",
input: []byte("3"), input: []byte("3"),
expectedGender: "Unspecified", executingSymbol: "set_unspecified",
expectCall: true, expectedGender: "unspecified",
}, expectCall: true,
{
name: "Empty Input",
input: []byte(""),
expectedGender: "",
expectCall: false,
}, },
} }
@ -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)
@ -538,13 +559,13 @@ func TestSetLanguage(t *testing.T) {
} }
// Define test cases // Define test cases
tests := []struct { tests := []struct {
name string name string
execPath []string execPath []string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
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

@ -23,4 +23,9 @@ func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) { func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
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.