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 823 additions and 349 deletions

View File

@ -61,8 +61,8 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn
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)
@ -89,11 +89,15 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn
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("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("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances) ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
return ussdHandlers, nil return ussdHandlers, nil
} }

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"time" "time"
"git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/config"
@ -24,6 +25,7 @@ type AccountServiceInterface interface {
CreateAccount(ctx context.Context) (*api.OKResponse, error) CreateAccount(ctx context.Context) (*api.OKResponse, error)
CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error)
TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error)
FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error)
} }
type AccountService struct { type AccountService struct {
@ -169,6 +171,23 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*api.OKResponse, e
return &okResponse, nil return &okResponse, nil
} }
// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
file, err := os.Open("sample_tokens.json")
if err != nil {
return nil, err
}
defer file.Close()
var holdings models.VoucherHoldingResponse
if err := json.NewDecoder(file).Decode(&holdings); err != nil {
return nil, err
}
return &holdings, nil
}
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) { func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
return &api.OKResponse{ return &api.OKResponse{
Ok: true, Ok: true,
@ -225,3 +244,31 @@ func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingI
} }
return trackResponse, nil return trackResponse, nil
} }
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
return &models.VoucherHoldingResponse{
Ok: true,
Result: struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
}{
Holdings: []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}{
{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF",
TokenDecimals: "6",
Balance: "2745987",
},
},
},
}, nil
}

View File

@ -22,6 +22,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 (
@ -187,33 +189,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)
@ -232,6 +207,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
@ -240,6 +218,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)
@ -249,11 +228,15 @@ 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
} }
@ -282,10 +265,10 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
return res, nil return res, nil
} }
// VerifyPin checks whether the confirmation PIN is similar to the account PIN // VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
// If similar, it sets the USERFLAG_PIN_SET flag allowing the user // If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN 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")
@ -297,12 +280,12 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
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)
@ -310,6 +293,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
} }
@ -628,14 +616,12 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
// CheckBalance retrieves the balance from the API using the "PublicKey" and sets // CheckBalance retrieves the balance of the active voucher and sets
// the balance as the result content // the balance as the result content
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
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")
@ -646,23 +632,25 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
l.AddDomain("default") l.AddDomain("default")
store := h.userdataStore store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
// get the active sym and active balance
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
balance := "0.00"
res.Content = l.Get("Balance: %s\n", balance)
return res, nil
}
return res, err
}
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil { if err != nil {
return res, err return res, err
} }
balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey)) res.Content = l.Get("Balance: %s\n", fmt.Sprintf("%s %s", activeBal, activeSym))
if err != nil {
return res, nil
}
if !balanceResponse.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Result.Balance
res.Content = l.Get("Balance: %s\n", balance)
return res, nil return res, nil
} }
@ -800,15 +788,13 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
balanceResp, err := h.accountService.CheckBalance(ctx, string(publicKey)) activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil { if err != nil {
return res, nil return res, err
} }
balance := balanceResp.Result.Balance
res.Content = balance res.Content = string(activeBal)
return res, nil return res, nil
} }
@ -817,54 +803,29 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
// it is not more than the current balance. // it is not more than the current balance.
func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
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")
} }
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amountStr := string(input) var balanceValue float64
balanceRes, err := h.accountService.CheckBalance(ctx, string(publicKey)) // retrieve the active balance
balanceStr := balanceRes.Result.Balance activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if !balanceRes.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
if err != nil { if err != nil {
return res, err return res, err
} }
res.Content = balanceStr balanceValue, err = strconv.ParseFloat(string(activeBal), 64)
res.FlagReset = append(res.FlagReset, flag_api_error)
// Parse the balance
balanceParts := strings.Split(balanceStr, " ")
if len(balanceParts) != 2 {
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
}
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
if err != nil { if err != nil {
return res, fmt.Errorf("failed to parse balance: %v", err) return res, err
} }
// Extract numeric part from input // Extract numeric part from the input amount
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`) amountStr := strings.TrimSpace(string(input))
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr)) inputAmount, err := strconv.ParseFloat(amountStr, 64)
if len(matches) < 2 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
return res, nil
}
inputAmount, err := strconv.ParseFloat(matches[1], 64)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount) res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr res.Content = amountStr
@ -877,12 +838,14 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places // Format the amount with 2 decimal places before saving
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr)) formattedAmount := fmt.Sprintf("%.2f", inputAmount)
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(formattedAmount))
if err != nil { if err != nil {
return res, err return res, err
} }
res.Content = fmt.Sprintf("%s", formattedAmount)
return res, nil return res, nil
} }
@ -925,9 +888,16 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
// retrieve the active symbol
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
return res, err
}
amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT) amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
res.Content = string(amount) res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym))
return res, nil return res, nil
} }
@ -953,7 +923,9 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT) recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT)
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(sessionId)) activeSym, _ := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
res.Content = l.Get("Your request has been sent. %s will receive %s %s from %s.", string(recipient), string(amount), string(activeSym), string(sessionId))
account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized") account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized")
if err != nil { if err != nil {
@ -1037,3 +1009,279 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// SetDefaultVoucher retrieves the current vouchers
// and sets the first as the default voucher, if no active voucher is set
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher")
// check if the user has an active sym
_, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
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(ctx, string(publicKey))
if err != nil {
return res, nil
}
// Return if there is no voucher
if len(vouchersResp.Result.Holdings) == 0 {
res.FlagSet = append(res.FlagSet, flag_no_active_voucher)
return res, nil
}
// Use only the first voucher
firstVoucher := vouchersResp.Result.Holdings[0]
defaultSym := firstVoucher.TokenSymbol
defaultBal := firstVoucher.Balance
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(defaultSym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(defaultBal))
if err != nil {
return res, err
}
return res, nil
}
return res, err
}
res.FlagReset = append(res.FlagReset, flag_no_active_voucher)
return res, nil
}
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
// them to gdbm
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, nil
}
// process voucher data
voucherSymbolList, voucherBalanceList := ProcessVouchers(vouchersResp.Result.Holdings)
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
err = prefixdb.Put(ctx, []byte("sym"), []byte(voucherSymbolList))
if err != nil {
return res, nil
}
err = prefixdb.Put(ctx, []byte("bal"), []byte(voucherBalanceList))
if err != nil {
return res, nil
}
return res, nil
}
// ProcessVouchers formats the holdings into symbol and balance lists.
func ProcessVouchers(holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}) (string, string) {
var numberedSymbols, numberedBalances []string
for i, voucher := range 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")
return voucherSymbolList, voucherBalanceList
}
// GetVoucherList fetches the list of vouchers and formats them
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
// Read vouchers from the store
store := h.userdataStore
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
voucherData, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil {
return res, nil
}
res.Content = string(voucherData)
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
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
inputStr := string(input)
if inputStr == "0" || inputStr == "99" {
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
return res, nil
}
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
// Retrieve the voucher symbol list
voucherSymbolList, err := prefixdb.Get(ctx, []byte("sym"))
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("bal"))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err)
}
// match the voucher symbol and balance with the input
matchedSymbol, matchedBalance := MatchVoucher(inputStr, string(voucherSymbolList), string(voucherBalanceList))
// If a match is found, write the temporary sym and balance
if matchedSymbol != "" && matchedBalance != "" {
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(matchedSymbol))
if err != nil {
return res, err
}
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(matchedBalance))
if err != nil {
return res, err
}
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance)
} else {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
}
return res, nil
}
// MatchVoucher finds the matching voucher symbol and balance based on the input.
func MatchVoucher(inputStr string, voucherSymbols, voucherBalances string) (string, string) {
// 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]
}
break
}
}
return matchedSymbol, matchedBalance
}
// SetVoucher retrieves the temporary sym and balance,
// sets them as the active data and
// clears the temporary data
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
// get the current temporary symbol
temporarySym, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM)
if err != nil {
return res, err
}
// get the current temporary balance
temporaryBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL)
if err != nil {
return res, err
}
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(temporarySym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(temporaryBal))
if err != nil {
return res, err
}
// reset the temporary symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(""))
if err != nil {
return res, err
}
// reset the temporary balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(""))
if err != nil {
return res, err
}
res.Content = string(temporarySym)
return res, nil
}

View File

@ -218,7 +218,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 {
@ -260,10 +260,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)
@ -481,37 +481,6 @@ func TestCheckIdentifier(t *testing.T) {
} }
} }
func TestMaxAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
// Define test data
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
expectedBalance := &models.BalanceResponse{
Ok: true,
}
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", publicKey).Return(expectedBalance, nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
accountService: mockCreateAccountService,
}
// Call the method
res, _ := h.MaxAmount(ctx, "max_amount", []byte("check_balance"))
//Assert that the balance that was set as the result content is what was returned by Check Balance
assert.Equal(t, expectedBalance.Result.Balance, res.Content)
}
func TestGetSender(t *testing.T) { func TestGetSender(t *testing.T) {
mockStore := new(mocks.MockUserDataStore) mockStore := new(mocks.MockUserDataStore)
@ -532,26 +501,30 @@ func TestGetSender(t *testing.T) {
} }
func TestGetAmount(t *testing.T) { func TestGetAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
// Define test data // Define test data
sessionId := "session123" sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
Amount := "0.03CELO" amount := "0.03"
activeSym := "SRF"
// Set up the expected behavior of the mock // Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(Amount), nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(activeSym), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(amount), nil)
// Create the Handlers instance with the mock store // Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: mockStore, userdataStore: mockDataStore,
} }
// Call the method // Call the method
res, _ := h.GetAmount(ctx, "get_amount", []byte("Getting amount...")) res, _ := h.GetAmount(ctx, "get_amount", []byte(""))
formattedAmount := fmt.Sprintf("%s %s", amount, activeSym)
//Assert that the retrieved amount is what was set as the content //Assert that the retrieved amount is what was set as the content
assert.Equal(t, Amount, res.Content) assert.Equal(t, formattedAmount, res.Content)
} }
@ -980,7 +953,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 {
@ -1029,7 +1002,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) {
@ -1037,8 +1010,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)
@ -1317,16 +1293,18 @@ func TestInitiateTransaction(t *testing.T) {
input []byte input []byte
Recipient []byte Recipient []byte
Amount []byte Amount []byte
ActiveSym []byte
status string status string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test initiate transaction", name: "Test initiate transaction",
Amount: []byte("0.002 CELO"), Amount: []byte("0.002"),
ActiveSym: []byte("SRF"),
Recipient: []byte("0x12415ass27192"), Recipient: []byte("0x12415ass27192"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagReset: []uint32{account_authorized_flag}, FlagReset: []uint32{account_authorized_flag},
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 CELO from 254712345678.", Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.",
}, },
}, },
} }
@ -1335,6 +1313,7 @@ func TestInitiateTransaction(t *testing.T) {
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return(tt.ActiveSym, nil)
// Call the method under test // Call the method under test
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input) res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input)
@ -1464,7 +1443,6 @@ func TestValidateAmount(t *testing.T) {
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")
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
mockDataStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService) mockCreateAccountService := new(mocks.MockAccountService)
@ -1480,66 +1458,33 @@ func TestValidateAmount(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
publicKey []byte activeBal []byte
balanceResponse *models.BalanceResponse balance string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test with valid amount", name: "Test with valid amount",
input: []byte("0.001"), input: []byte("4.10"),
balanceResponse: &models.BalanceResponse{ activeBal: []byte("5"),
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{ expectedResult: resource.Result{
Content: "0.001", Content: "4.10",
FlagReset: []uint32{flag_api_error},
}, },
}, },
{ {
name: "Test with amount larger than balance", name: "Test with amount larger than active balance",
input: []byte("0.02"), input: []byte("5.02"),
balanceResponse: &models.BalanceResponse{ activeBal: []byte("5"),
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount}, FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error}, Content: "5.02",
Content: "0.02",
}, },
}, },
{ {
name: "Test with invalid amount", name: "Test with invalid amount format",
input: []byte("0.02ms"), input: []byte("0.02ms"),
balanceResponse: &models.BalanceResponse{ activeBal: []byte("5"),
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount}, FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02ms", Content: "0.02ms",
}, },
}, },
@ -1547,23 +1492,23 @@ func TestValidateAmount(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Mock behavior for active balance retrieval
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL).Return(tt.activeBal, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) // Mock behavior for storing the amount (if valid)
mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balanceResponse, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe() 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)
// Assert that no errors occurred // Assert no errors occurred
assert.NoError(t, err) assert.NoError(t, err)
//Assert that the account created flag has been set to the result // Assert the result matches the expected result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") assert.Equal(t, tt.expectedResult, res, "Expected result should match actual result")
// Assert that expectations were met // Assert all expectations were met
mockDataStore.AssertExpectations(t) mockDataStore.AssertExpectations(t)
}) })
} }
} }
@ -1627,80 +1572,52 @@ func TestValidateRecipient(t *testing.T) {
} }
func TestCheckBalance(t *testing.T) { func TestCheckBalance(t *testing.T) {
sessionId := "session123"
publicKey := "0X13242618721"
fm, _ := NewFlagManager(flagsPath)
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct { tests := []struct {
name string name string
balanceResonse *models.BalanceResponse sessionId string
publicKey string
activeSym string
activeBal string
expectedResult resource.Result expectedResult resource.Result
expectError bool
}{ }{
{ {
name: "Test when checking a balance is not a success", name: "User with active sym",
balanceResonse: &models.BalanceResponse{ sessionId: "session456",
Ok: false, publicKey: "0X98765432109",
Result: struct { activeSym: "ETH",
Balance string `json:"balance"` activeBal: "1.5",
Nonce json.Number `json:"nonce"` expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"},
}{ expectError: false,
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when checking a balance is a success",
balanceResonse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
Content: "Balance: 0.003 CELO\n",
FlagReset: []uint32{flag_api_error},
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore) mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService) mockAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16) ctx := context.WithValue(context.Background(), "SessionId", tt.sessionId)
// Create the Handlers instance with the mock store
h := &Handlers{ h := &Handlers{
userdataStore: mockDataStore, userdataStore: mockDataStore,
flagManager: fm.parser, accountService: mockAccountService,
st: mockState,
accountService: mockCreateAccountService,
} }
// Set up the expected behavior of the mock // Mock for user with active sym
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(tt.activeSym), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil) mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_BAL).Return([]byte(tt.activeBal), nil)
// Call the method res, err := h.CheckBalance(ctx, "check_balance", []byte(""))
res, _ := h.CheckBalance(ctx, "check_balance", []byte(""))
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, res, "Result should match expected output")
}
// Assert that expectations were met
mockDataStore.AssertExpectations(t) mockDataStore.AssertExpectations(t)
mockAccountService.AssertExpectations(t)
//Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should contain flags set according to user input")
}) })
} }
} }
@ -1844,42 +1761,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"
@ -1927,7 +1808,40 @@ func TestConfirmPin(t *testing.T) {
}) })
} }
}
func TestSetVoucher(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
temporarySym := []byte("tempSym")
temporaryBal := []byte("tempBal")
// Set expectations for the mock data store
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM).Return(temporarySym, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL).Return(temporaryBal, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM, temporarySym).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL, temporaryBal).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte("")).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte("")).Return(nil)
h := &Handlers{
userdataStore: mockDataStore,
}
// Call the method under test
res, err := h.SetVoucher(ctx, "someSym", []byte{})
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the result content is correct
assert.Equal(t, string(temporarySym), res.Content)
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
} }
func TestFetchCustodialBalances(t *testing.T) { func TestFetchCustodialBalances(t *testing.T) {

View File

@ -32,3 +32,9 @@ func (m *MockAccountService) TrackAccountStatus(ctx context.Context,publicKey st
args := m.Called(publicKey) args := m.Called(publicKey)
return args.Get(0).(*api.OKResponse), args.Error(1) return args.Get(0).(*api.OKResponse), args.Error(1)
} }
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1)
}

View File

@ -0,0 +1,18 @@
package models
type ApiResponse struct {
Review

These should be gotten from the api provider go code instead, #106

Since we are introducing new ones here, may as well go directly to the source.

These should be gotten from the api provider go code instead, https://git.grassecon.net/urdt/ussd/issues/106 Since we are introducing new ones here, may as well go directly to the source.
OK bool `json:"ok"`
Description string `json:"description"`
Result Result `json:"result"`
}
type Result struct {
Holdings []Holding `json:"holdings"`
}
type Holding struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}

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"`
}

View File

@ -23,6 +23,11 @@ const (
DATA_RECIPIENT DATA_RECIPIENT
DATA_AMOUNT DATA_AMOUNT
DATA_TEMPORARY_PIN DATA_TEMPORARY_PIN
DATA_VOUCHER_LIST
DATA_TEMPORARY_SYM
DATA_ACTIVE_SYM
DATA_TEMPORARY_BAL
DATA_ACTIVE_BAL
) )
func typToBytes(typ DataTyp) []byte { func typToBytes(typ DataTyp) []byte {

View File

@ -5,7 +5,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -33,7 +33,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -42,7 +42,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -70,7 +70,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -79,7 +79,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -116,7 +116,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -125,7 +125,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -162,7 +162,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -171,7 +171,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -203,7 +203,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -212,7 +212,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -244,7 +244,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
@ -254,7 +254,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -286,7 +286,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -295,7 +295,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -327,7 +327,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -336,7 +336,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -368,7 +368,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -377,7 +377,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -409,7 +409,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
}, },
@ -418,7 +418,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",
@ -446,7 +446,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
} }
] ]
} }

View File

@ -43,6 +43,39 @@ func extractPublicKey(response []byte) string {
return "" return ""
} }
// Extracts the balance value from the engine response.
func extractBalance(response []byte) string {
// Regex to match "Balance: <amount> <symbol>" followed by a newline
re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
}
return ""
}
// Extracts the Maximum amount value from the engine response.
func extractMaxAmount(response []byte) string {
// Regex to match "Maximum amount: <amount>" followed by a newline
re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // "<amount>"
}
return ""
}
// Extracts the send amount value from the engine response.
func extractSendAmount(response []byte) string {
// Regex to match the pattern "will receive X.XX SYM from"
re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // Returns "X.XX SYM"
}
return ""
}
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
sessionID = GenerateSessionId() sessionID = GenerateSessionId()
defer func() { defer func() {
@ -154,6 +187,12 @@ func TestMainMenuHelp(t *testing.T) {
} }
b := w.Bytes() b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b) match, err := step.MatchesExpectedContent(b)
if err != nil { if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
@ -189,6 +228,12 @@ func TestMainMenuQuit(t *testing.T) {
} }
b := w.Bytes() b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b) match, err := step.MatchesExpectedContent(b)
if err != nil { if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
@ -225,8 +270,13 @@ func TestMyAccount_MyAddress(t *testing.T) {
} }
b := w.Bytes() b := w.Bytes()
balance := extractBalance(b)
publicKey := extractPublicKey(b) publicKey := extractPublicKey(b)
expectedContent := bytes.Replace([]byte(step.ExpectedContent), []byte("{public_key}"), []byte(publicKey), -1)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
step.ExpectedContent = string(expectedContent) step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b) match, err := step.MatchesExpectedContent(b)
if err != nil { if err != nil {
@ -240,6 +290,52 @@ func TestMyAccount_MyAddress(t *testing.T) {
} }
} }
func TestMainMenuSend(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
max_amount := extractMaxAmount(b)
send_amount := extractSendAmount(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestGroups(t *testing.T) { func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile) groups, err := driver.LoadTestGroups(groupTestFile)
if err != nil { if err != nil {
@ -265,6 +361,13 @@ func TestGroups(t *testing.T) {
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err) t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
} }
b := w.Bytes() b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(tt.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
tt.ExpectedContent = string(expectedContent)
match, err := tt.MatchesExpectedContent(b) match, err := tt.MatchesExpectedContent(b)
if err != nil { if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err) t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
@ -272,7 +375,6 @@ func TestGroups(t *testing.T) {
if !match { if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b) t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
} }
}) })
} }
} }

View File

@ -57,7 +57,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@ -73,19 +73,19 @@
}, },
{ {
"input": "065656", "input": "065656",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back" "expectedContent": "{max_amount}\nEnter amount:\n0:Back"
}, },
{ {
"input": "0.1", "input": "10000000",
"expectedContent": "Amount 0.1 is invalid, please try again:\n1:retry\n9:Quit" "expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back" "expectedContent": "{max_amount}\nEnter amount:\n0:Back"
}, },
{ {
"input": "0.001", "input": "1.00",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" "expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
}, },
{ {
"input": "1222", "input": "1222",
@ -93,11 +93,11 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" "expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive 0.001 from {public_key}." "expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}."
} }
] ]
}, },
@ -106,7 +106,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "4", "input": "4",
@ -119,7 +119,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "9", "input": "9",
@ -132,7 +132,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "3",

44
sample_tokens.json Normal file
View File

@ -0,0 +1,44 @@
{
"ok": true,
"description": "Token holdings with current balances",
"result": {
"holdings": [
{
"contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
"tokenSymbol": "FSPTST",
"tokenDecimals": "6",
"balance": "8869964242"
},
{
"contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA",
"tokenSymbol": "GEO",
"tokenDecimals": "6",
"balance": "9884"
},
{
"contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273",
"tokenSymbol": "MFNK",
"tokenDecimals": "6",
"balance": "19788697"
},
{
"contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83",
"tokenSymbol": "MILO",
"tokenDecimals": "6",
"balance": "75"
},
{
"contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
"tokenSymbol": "SOHAIL",
"tokenDecimals": "6",
"balance": "27874115"
},
{
"contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958",
"tokenSymbol": "SRF",
"tokenDecimals": "6",
"balance": "2745987"
}
]
}
}

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,5 +1,6 @@
LOAD reset_transaction_amount 0 LOAD reset_transaction_amount 0
LOAD max_amount 10 LOAD max_amount 10
RELOAD max_amount
MAP max_amount MAP max_amount
MOUT back 0 MOUT back 0
HALT HALT
@ -10,5 +11,5 @@ CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0 INCMP _ 0
LOAD get_recipient 12 LOAD get_recipient 12
LOAD get_sender 64 LOAD get_sender 64
LOAD get_amount 12 LOAD get_amount 32
INCMP transaction_pin * INCMP transaction_pin *

View File

@ -1,4 +1,4 @@
LOAD save_pin 0 LOAD save_temporary_pin 6
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 6
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

@ -1,8 +1,8 @@
msgid "Your account balance is %s" msgid "Your account balance is %s"
msgstr "Salio lako ni %s" msgstr "Salio lako ni %s"
msgid "Your request has been sent. %s will receive %s from %s." msgid "Your request has been sent. %s will receive %s %s from %s."
msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s." msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
msgid "Thank you for using Sarafu. Goodbye!" msgid "Thank you for using Sarafu. Goodbye!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"

View File

@ -1,5 +1,9 @@
LOAD set_default_voucher 8
RELOAD set_default_voucher
LOAD check_balance 64 LOAD check_balance 64
RELOAD check_balance RELOAD check_balance
LOAD check_vouchers 10
RELOAD check_vouchers
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
MAP check_balance MAP check_balance
MOUT send 1 MOUT send 1
@ -9,7 +13,7 @@ MOUT help 4
MOUT quit 9 MOUT quit 9
HALT HALT
INCMP send 1 INCMP send 1
INCMP quit 2 INCMP my_vouchers 2
INCMP my_account 3 INCMP my_account 3
INCMP help 4 INCMP help 4
INCMP quit 9 INCMP quit 9

View File

@ -0,0 +1 @@
My vouchers

View File

@ -0,0 +1,8 @@
LOAD reset_account_authorized 16
RELOAD reset_account_authorized
MOUT select_voucher 1
MOUT voucher_details 2
MOUT back 0
HALT
INCMP _ 0
INCMP select_voucher 1

View File

@ -0,0 +1 @@
You need a voucher to send

View File

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

View File

@ -0,0 +1 @@
Unahitaji sarafu kutuma

View File

@ -14,4 +14,6 @@ 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_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth
flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid
flag,flag_incorrect_voucher,24,this is set when the selected voucher is invalid
flag,flag_api_call_error,25,this is set when communication to an external service fails flag,flag_api_call_error,25,this is set when communication to an external service fails
flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher

1 flag flag_language_set 8 checks whether the user has set their prefered language
14 flag flag_allow_update 21 this is set to allow a user to update their profile data
15 flag flag_single_edit 22 this is set to allow a user to edit a single profile item such as year of birth
16 flag flag_incorrect_date_format 23 this is set when the given year of birth is invalid
17 flag flag_incorrect_voucher 24 this is set when the selected voucher is invalid
18 flag flag_api_call_error 25 this is set when communication to an external service fails this is set when communication to an external service fails
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher

View File

@ -0,0 +1,2 @@
Select number or symbol from your vouchers:
{{.get_vouchers}}

View File

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

View File

@ -0,0 +1 @@
Select voucher

View File

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

View File

@ -1,4 +1,5 @@
LOAD transaction_reset 0 LOAD transaction_reset 0
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0 MOUT back 0
HALT HALT
LOAD validate_recipient 20 LOAD validate_recipient 20

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
Voucher details

View File

@ -0,0 +1 @@
Success! {{.set_voucher}} is now your active voucher.

View File

@ -0,0 +1,10 @@
LOAD reset_incorrect 6
CATCH incorrect_pin flag_incorrect_pin 1
CATCH _ flag_account_authorized 0
LOAD set_voucher 12
MAP set_voucher
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Hongera! {{.set_voucher}} ni Sarafu inayotumika sasa.