forked from urdt/ussd
Merge pull request 'wip-account-creation' (#4) from wip-account-creation into master
Reviewed-on: urdt/ussd#4 Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
This commit is contained in:
commit
ef0c207fc4
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,2 +1,6 @@
|
|||||||
**/*.env
|
**/*.env
|
||||||
covprofile
|
covprofile
|
||||||
|
go.work*
|
||||||
|
**/*/*.bin
|
||||||
|
**/*/.state/
|
||||||
|
cmd/.state/
|
||||||
|
146
cmd/main.go
Normal file
146
cmd/main.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/cache"
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/persist"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.defalsify.org/vise.git/state"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var dir string
|
||||||
|
var root string
|
||||||
|
var size uint
|
||||||
|
var sessionId string
|
||||||
|
flag.StringVar(&dir, "d", ".", "resource dir to read from")
|
||||||
|
flag.UintVar(&size, "s", 0, "max size of output")
|
||||||
|
flag.StringVar(&root, "root", "root", "entry point symbol")
|
||||||
|
flag.StringVar(&sessionId, "session-id", "default", "session id")
|
||||||
|
flag.Parse()
|
||||||
|
fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
st := state.NewState(16)
|
||||||
|
st.UseDebug()
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_LANGUAGE_SET, "LANGUAGE_CHANGE")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATED, "ACCOUNT_CREATED")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_SUCCESS, "ACCOUNT_SUCCESS")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_PENDING, "ACCOUNT_PENDING")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_INCORRECTPIN, "INCORRECTPIN")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_INCORRECTDATEFORMAT, "INVALIDDATEFORMAT")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT, "INVALIDRECIPIENT")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_PINMISMATCH, "PINMISMATCH")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_PIN_SET, "PIN_SET")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE, "INVALIDRECIPIENT_WITH_INVITE")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_INVALID_AMOUNT, "INVALIDAMOUNT")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_ALLOW_UPDATE, "UNLOCKFORUPDATE")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "VALIDPIN")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "ACCOUNTUNLOCKED")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATION_FAILED, "ACCOUNT_CREATION_FAILED")
|
||||||
|
state.FlagDebugger.Register(models.USERFLAG_SINGLE_EDIT, "SINGLEEDIT")
|
||||||
|
|
||||||
|
rfs := resource.NewFsResource(scriptDir)
|
||||||
|
ca := cache.NewCache()
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
SessionId: sessionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := path.Join(scriptDir, ".state")
|
||||||
|
err := os.MkdirAll(dp, 0700)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
pr := persist.NewFsPersister(dp)
|
||||||
|
en, err := engine.NewPersistedEngine(ctx, cfg, pr, rfs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
pr = pr.WithContent(&st, ca)
|
||||||
|
err = pr.Save(cfg.SessionId)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to save state with error: %v\n", err)
|
||||||
|
}
|
||||||
|
en, err = engine.NewPersistedEngine(ctx, cfg, pr, rfs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "engine create exited with error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fp := path.Join(dp, sessionId)
|
||||||
|
|
||||||
|
ussdHandlers := ussd.NewHandlers(fp, &st)
|
||||||
|
|
||||||
|
rfs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
|
||||||
|
rfs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||||
|
rfs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
|
||||||
|
rfs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
|
||||||
|
rfs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
|
||||||
|
rfs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
|
||||||
|
rfs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
|
||||||
|
rfs.AddLocalFunc("quit", ussdHandlers.Quit)
|
||||||
|
rfs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
|
||||||
|
rfs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
|
||||||
|
rfs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
|
||||||
|
rfs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
|
||||||
|
rfs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
|
||||||
|
rfs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
|
||||||
|
rfs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
|
||||||
|
rfs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
|
||||||
|
rfs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
|
||||||
|
rfs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
|
||||||
|
rfs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
|
||||||
|
rfs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
|
||||||
|
rfs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
|
||||||
|
rfs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
|
||||||
|
rfs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
|
||||||
|
rfs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
|
||||||
|
rfs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
|
||||||
|
rfs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
|
||||||
|
rfs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
|
||||||
|
rfs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
|
||||||
|
rfs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
|
||||||
|
rfs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
|
||||||
|
rfs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
|
||||||
|
rfs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
|
||||||
|
|
||||||
|
cont, err := en.Init(ctx)
|
||||||
|
en.SetDebugger(engine.NewSimpleDebug(nil))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !cont {
|
||||||
|
_, err = en.WriteResult(ctx, os.Stdout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "dead init write error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = en.Finish()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "engine finish error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte{0x0a})
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
err = engine.Loop(ctx, en, os.Stdin, os.Stdout)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
10
config/config.go
Normal file
10
config/config.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const (
|
||||||
|
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create"
|
||||||
|
TrackStatusURL = "https://custodial.sarafu.africa/api/track/"
|
||||||
|
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
|
||||||
|
)
|
||||||
|
|
1
go-vise
Submodule
1
go-vise
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 1f47a674d95380be8c387f410f0342eb72357df5
|
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
|||||||
module git.grassecon.net/urdt/ussd
|
module git.grassecon.net/urdt/ussd
|
||||||
|
|
||||||
go 1.22.6
|
go 1.22.6
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.9.0 // indirect
|
||||||
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
112
internal/handlers/server/accountservice.go
Normal file
112
internal/handlers/server/accountservice.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountServiceInterface interface {
|
||||||
|
CheckBalance(publicKey string) (string, error)
|
||||||
|
CreateAccount() (*models.AccountResponse, error)
|
||||||
|
CheckAccountStatus(trackingId string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
|
||||||
|
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
|
||||||
|
// AccountResponse struct can be used here to check the account status during a transaction.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
|
||||||
|
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||||
|
// If no error occurs, this will be nil.
|
||||||
|
//
|
||||||
|
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
|
||||||
|
resp, err := http.Get(config.TrackStatusURL + trackingId)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackResp models.TrackStatusResponse
|
||||||
|
err = json.Unmarshal(body, &trackResp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := trackResp.Result.Transaction.Status
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
|
||||||
|
// Parameters:
|
||||||
|
// - publicKey: The public key associated with the account whose balance needs to be checked.
|
||||||
|
func (as *AccountService) CheckBalance(publicKey string) (string, error) {
|
||||||
|
|
||||||
|
resp, err := http.Get(config.BalanceURL + publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return "0.0", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "0.0", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var balanceResp models.BalanceResponse
|
||||||
|
err = json.Unmarshal(body, &balanceResp)
|
||||||
|
if err != nil {
|
||||||
|
return "0.0", err
|
||||||
|
}
|
||||||
|
|
||||||
|
balance := balanceResp.Result.Balance
|
||||||
|
return balance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//CreateAccount creates a new account in the custodial system.
|
||||||
|
// Returns:
|
||||||
|
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
|
||||||
|
// If there is an error during the request or processing, this will be nil.
|
||||||
|
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
|
||||||
|
// If no error occurs, this will be nil.
|
||||||
|
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||||
|
resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountResp models.AccountResponse
|
||||||
|
err = json.Unmarshal(body, &accountResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &accountResp, nil
|
||||||
|
}
|
779
internal/handlers/ussd/menuhandler.go
Normal file
779
internal/handlers/ussd/menuhandler.go
Normal file
@ -0,0 +1,779 @@
|
|||||||
|
package ussd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.defalsify.org/vise.git/state"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||||
|
"gopkg.in/leonelquinteros/gotext.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptDir = path.Join("services", "registration")
|
||||||
|
translationDir = path.Join(scriptDir, "locale")
|
||||||
|
)
|
||||||
|
|
||||||
|
type FSData struct {
|
||||||
|
Path string
|
||||||
|
St *state.State
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Handlers struct {
|
||||||
|
fs *FSData
|
||||||
|
accountFileHandler utils.AccountFileHandlerInterface
|
||||||
|
accountService server.AccountServiceInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandlers(path string, st *state.State) *Handlers {
|
||||||
|
return &Handlers{
|
||||||
|
fs: &FSData{
|
||||||
|
Path: path,
|
||||||
|
St: st,
|
||||||
|
},
|
||||||
|
accountFileHandler: utils.NewAccountFileHandler(path + "_data"),
|
||||||
|
accountService: &server.AccountService{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Define the regex pattern as a constant
|
||||||
|
const pinPattern = `^\d{4}$`
|
||||||
|
|
||||||
|
// isValidPIN checks whether the given input is a 4 digit number
|
||||||
|
func isValidPIN(pin string) bool {
|
||||||
|
match, _ := regexp.MatchString(pinPattern, pin)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLanguage sets the language across the menu
|
||||||
|
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
inputStr := string(input)
|
||||||
|
res := resource.Result{}
|
||||||
|
switch inputStr {
|
||||||
|
case "0":
|
||||||
|
res.FlagSet = []uint32{state.FLAG_LANG}
|
||||||
|
res.Content = "eng"
|
||||||
|
case "1":
|
||||||
|
res.FlagSet = []uint32{state.FLAG_LANG}
|
||||||
|
res.Content = "swa"
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_LANGUAGE_SET)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAccount checks if any account exists on the JSON data file, and if not
|
||||||
|
// creates an account on the API,
|
||||||
|
// sets the default values and flags
|
||||||
|
func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
err := h.accountFileHandler.EnsureFileExists()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if an account exists, return to prevent duplicate account creation
|
||||||
|
existingAccountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if existingAccountData != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountResp, err := h.accountService.CreateAccount()
|
||||||
|
if err != nil {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_CREATION_FAILED)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountData := map[string]string{
|
||||||
|
"TrackingId": accountResp.Result.TrackingId,
|
||||||
|
"PublicKey": accountResp.Result.PublicKey,
|
||||||
|
"CustodialId": accountResp.Result.CustodialId.String(),
|
||||||
|
"Status": "PENDING",
|
||||||
|
"Gender": "Not provided",
|
||||||
|
"YOB": "Not provided",
|
||||||
|
"Location": "Not provided",
|
||||||
|
"Offerings": "Not provided",
|
||||||
|
"FirstName": "Not provided",
|
||||||
|
"FamilyName": "Not provided",
|
||||||
|
}
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_CREATED)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePin persists the user's PIN choice into the filesystem
|
||||||
|
func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
accountPIN := string(input)
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the PIN is a 4-digit number
|
||||||
|
if !isValidPIN(accountPIN) {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTPIN)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN)
|
||||||
|
accountData["AccountPIN"] = accountPIN
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information.
|
||||||
|
func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
menuOption := string(input)
|
||||||
|
switch menuOption {
|
||||||
|
case "2":
|
||||||
|
res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT)
|
||||||
|
case "3":
|
||||||
|
res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT)
|
||||||
|
case "4":
|
||||||
|
res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT)
|
||||||
|
default:
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_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
|
||||||
|
func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(input, []byte(accountData["AccountPIN"])) {
|
||||||
|
res.FlagSet = []uint32{models.USERFLAG_VALIDPIN}
|
||||||
|
res.FlagReset = []uint32{models.USERFLAG_PINMISMATCH}
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_PIN_SET)
|
||||||
|
} else {
|
||||||
|
res.FlagSet = []uint32{models.USERFLAG_PINMISMATCH}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//codeFromCtx retrieves language codes from the context that can be used for handling translations
|
||||||
|
func codeFromCtx(ctx context.Context) string {
|
||||||
|
var code string
|
||||||
|
engine.Logg.DebugCtxf(ctx, "in msg", "ctx", ctx, "val", code)
|
||||||
|
if ctx.Value("Language") != nil {
|
||||||
|
lang := ctx.Value("Language").(lang.Language)
|
||||||
|
code = lang.Code
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFirstname updates the first name in a JSON data file with the provided input.
|
||||||
|
func (h *Handlers) SaveFirstname(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
if len(input) > 0 {
|
||||||
|
name := string(input)
|
||||||
|
accountData["FirstName"] = name
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFamilyname updates the family name in a JSON data file with the provided input.
|
||||||
|
func (h *Handlers) SaveFamilyname(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
if len(input) > 0 {
|
||||||
|
secondname := string(input)
|
||||||
|
accountData["FamilyName"] = secondname
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveYOB updates the Year of Birth(YOB) in a JSON data file with the provided input.
|
||||||
|
func (h *Handlers) SaveYob(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
yob := string(input)
|
||||||
|
if len(yob) == 4 {
|
||||||
|
yob := string(input)
|
||||||
|
accountData["YOB"] = yob
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveLocation updates the location in a JSON data file with the provided input.
|
||||||
|
func (h *Handlers) SaveLocation(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) > 0 {
|
||||||
|
location := string(input)
|
||||||
|
accountData["Location"] = location
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveGender updates the gender in a JSON data file with the provided input.
|
||||||
|
func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) > 0 {
|
||||||
|
gender := string(input)
|
||||||
|
|
||||||
|
switch gender {
|
||||||
|
case "1":
|
||||||
|
gender = "Male"
|
||||||
|
case "2":
|
||||||
|
gender = "Female"
|
||||||
|
case "3":
|
||||||
|
gender = "Unspecified"
|
||||||
|
}
|
||||||
|
accountData["Gender"] = gender
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveOfferings updates the offerings(goods and services provided by the user) in a JSON data file with the provided input.
|
||||||
|
func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) > 0 {
|
||||||
|
offerings := string(input)
|
||||||
|
accountData["Offerings"] = offerings
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetAllowUpdate resets the allowupdate flag that allows a user to update profile data.
|
||||||
|
func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ALLOW_UPDATE)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
|
||||||
|
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckIdentifier retrieves the PublicKey from the JSON data file.
|
||||||
|
func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Content = accountData["PublicKey"]
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize attempts to unlock the next sequential nodes by verifying the provided PIN against the already set PIN.
|
||||||
|
// It sets the required flags that control the flow.
|
||||||
|
func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
pin := string(input)
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) == 4 {
|
||||||
|
if pin != accountData["AccountPIN"] {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTPIN)
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
if h.fs.St.MatchFlag(models.USERFLAG_ACCOUNT_AUTHORIZED, false) {
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN)
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
} else {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt.
|
||||||
|
func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAccountStatus queries the API using the TrackingId and sets flags
|
||||||
|
// based on the account status
|
||||||
|
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := h.accountService.CheckAccountStatus(accountData["TrackingId"])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error checking account status:", err)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accountData["Status"] = status
|
||||||
|
|
||||||
|
if status == "SUCCESS" {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_SUCCESS)
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_PENDING)
|
||||||
|
} else {
|
||||||
|
res.FlagReset = append(res.FlagSet, models.USERFLAG_ACCOUNT_SUCCESS)
|
||||||
|
res.FlagSet = append(res.FlagReset, models.USERFLAG_ACCOUNT_PENDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit displays the Thank you message and exits the menu
|
||||||
|
func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
code := codeFromCtx(ctx)
|
||||||
|
l := gotext.NewLocale(translationDir, code)
|
||||||
|
l.AddDomain("default")
|
||||||
|
|
||||||
|
res.Content = l.Get("Thank you for using Sarafu. Goodbye!")
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyYob verifies the length of the given input
|
||||||
|
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
date := string(input)
|
||||||
|
_, err := strconv.Atoi(date)
|
||||||
|
if err != nil {
|
||||||
|
// If conversion fails, input is not numeric
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTDATEFORMAT)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(date) == 4 {
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTDATEFORMAT)
|
||||||
|
} else {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTDATEFORMAT)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetIncorrectYob resets the incorrect date format after a new attempt
|
||||||
|
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTDATEFORMAT)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckBalance retrieves the balance from the API using the "PublicKey" and sets
|
||||||
|
// the balance as the result content
|
||||||
|
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
balance, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
res.Content = balance
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRecipient validates that the given input is a valid phone number.
|
||||||
|
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
recipient := string(input)
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipient != "0" {
|
||||||
|
// mimic invalid number check
|
||||||
|
if recipient == "000" {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_RECIPIENT)
|
||||||
|
res.Content = recipient
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accountData["Recipient"] = recipient
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionReset resets the previous transaction data (Recipient and Amount)
|
||||||
|
// as well as the invalid flags
|
||||||
|
func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the transaction
|
||||||
|
accountData["Recipient"] = ""
|
||||||
|
accountData["Amount"] = ""
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INVALID_RECIPIENT, models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetTransactionAmount resets the transaction amount and invalid flag
|
||||||
|
func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the amount
|
||||||
|
accountData["Amount"] = ""
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_INVALID_AMOUNT)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxAmount gets the current balance from the API and sets it as
|
||||||
|
// the result content.
|
||||||
|
func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
balance, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Content = balance
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAmount ensures that the given input is a valid amount and that
|
||||||
|
// it is not more than the current balance.
|
||||||
|
func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
amountStr := string(input)
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
balanceStr, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
res.Content = balanceStr
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return res, fmt.Errorf("failed to parse balance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract numeric part from input
|
||||||
|
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
|
||||||
|
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
|
||||||
|
if len(matches) < 2 {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT)
|
||||||
|
res.Content = amountStr
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inputAmount, err := strconv.ParseFloat(matches[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT)
|
||||||
|
res.Content = amountStr
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if inputAmount > balanceValue {
|
||||||
|
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT)
|
||||||
|
res.Content = amountStr
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
|
||||||
|
accountData["Amount"] = res.Content
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecipient returns the transaction recipient from a JSON data file.
|
||||||
|
func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Content = accountData["Recipient"]
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfileInfo retrieves and formats the profile information of a user from a JSON data file.
|
||||||
|
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
var age string
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if accountData["FirstName"] == "Not provided" || accountData["FamilyName"] == "Not provided" {
|
||||||
|
name = "Not provided"
|
||||||
|
} else {
|
||||||
|
name = accountData["FirstName"] + " " + accountData["FamilyName"]
|
||||||
|
}
|
||||||
|
|
||||||
|
gender := accountData["Gender"]
|
||||||
|
yob := accountData["YOB"]
|
||||||
|
location := accountData["Location"]
|
||||||
|
offerings := accountData["Offerings"]
|
||||||
|
if yob == "Not provided" {
|
||||||
|
age = "Not provided"
|
||||||
|
} else {
|
||||||
|
ageInt, err := strconv.Atoi(yob)
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
age = strconv.Itoa(utils.CalculateAgeWithYOB(ageInt))
|
||||||
|
}
|
||||||
|
formattedData := fmt.Sprintf("Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", name, gender, age, location, offerings)
|
||||||
|
res.Content = formattedData
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSender retrieves the public key from a JSON data file.
|
||||||
|
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Content = accountData["PublicKey"]
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAmount retrieves the amount from a JSON data file.
|
||||||
|
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Content = accountData["Amount"]
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuickWithBalance retrieves the balance for a given public key from the custodial balance API endpoint before
|
||||||
|
// gracefully exiting the session.
|
||||||
|
func (h *Handlers) QuitWithBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
code := codeFromCtx(ctx)
|
||||||
|
l := gotext.NewLocale(translationDir, code)
|
||||||
|
l.AddDomain("default")
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
balance, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
||||||
|
if err != nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
res.Content = l.Get("Your account balance is %s", balance)
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitiateTransaction returns a confirmation and resets the transaction data
|
||||||
|
// on the JSON file.
|
||||||
|
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
|
res := resource.Result{}
|
||||||
|
code := codeFromCtx(ctx)
|
||||||
|
l := gotext.NewLocale(translationDir, code)
|
||||||
|
l.AddDomain("default")
|
||||||
|
|
||||||
|
accountData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Use the amount, recipient and sender to call the API and initialize the transaction
|
||||||
|
|
||||||
|
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", accountData["Recipient"], accountData["Amount"], accountData["PublicKey"])
|
||||||
|
|
||||||
|
// reset the transaction
|
||||||
|
accountData["Recipient"] = ""
|
||||||
|
accountData["Amount"] = ""
|
||||||
|
|
||||||
|
err = h.accountFileHandler.WriteAccountData(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
||||||
|
return res, nil
|
||||||
|
}
|
878
internal/handlers/ussd/menuhandler_test.go
Normal file
878
internal/handlers/ussd/menuhandler_test.go
Normal file
@ -0,0 +1,878 @@
|
|||||||
|
package ussd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers/ussd/mocks"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||||
|
"github.com/alecthomas/assert/v2"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockAccountService implements AccountServiceInterface for testing
|
||||||
|
type MockAccountService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*models.AccountResponse), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
|
||||||
|
args := m.Called(publicKey)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
|
||||||
|
args := m.Called(trackingId)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_create_account")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up after the test run
|
||||||
|
|
||||||
|
sessionID := "07xxxxxxxx"
|
||||||
|
|
||||||
|
// Set up the data file path using the session ID
|
||||||
|
accountFilePath := filepath.Join(tempDir, sessionID+"_data")
|
||||||
|
|
||||||
|
// Initialize account file handler
|
||||||
|
accountFileHandler := utils.NewAccountFileHandler(accountFilePath)
|
||||||
|
|
||||||
|
// Create a mock account service
|
||||||
|
mockAccountService := &MockAccountService{}
|
||||||
|
mockAccountResponse := &models.AccountResponse{
|
||||||
|
Ok: true,
|
||||||
|
Result: struct {
|
||||||
|
CustodialId json.Number `json:"custodialId"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
TrackingId string `json:"trackingId"`
|
||||||
|
}{
|
||||||
|
CustodialId: "test-custodial-id",
|
||||||
|
PublicKey: "test-public-key",
|
||||||
|
TrackingId: "test-tracking-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up expectations for the mock account service
|
||||||
|
mockAccountService.On("CreateAccount").Return(mockAccountResponse, nil)
|
||||||
|
|
||||||
|
// Initialize Handlers with mock account service
|
||||||
|
h := &Handlers{
|
||||||
|
fs: &FSData{Path: accountFilePath},
|
||||||
|
accountFileHandler: accountFileHandler,
|
||||||
|
accountService: mockAccountService,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
existingData map[string]string
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedData map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "New account creation",
|
||||||
|
existingData: nil,
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagSet: []uint32{models.USERFLAG_ACCOUNT_CREATED},
|
||||||
|
},
|
||||||
|
expectedData: map[string]string{
|
||||||
|
"TrackingId": "test-tracking-id",
|
||||||
|
"PublicKey": "test-public-key",
|
||||||
|
"CustodialId": "test-custodial-id",
|
||||||
|
"Status": "PENDING",
|
||||||
|
"Gender": "Not provided",
|
||||||
|
"YOB": "Not provided",
|
||||||
|
"Location": "Not provided",
|
||||||
|
"Offerings": "Not provided",
|
||||||
|
"FirstName": "Not provided",
|
||||||
|
"FamilyName": "Not provided",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Existing account",
|
||||||
|
existingData: map[string]string{
|
||||||
|
"TrackingId": "test-tracking-id",
|
||||||
|
"PublicKey": "test-public-key",
|
||||||
|
"CustodialId": "test-custodial-id",
|
||||||
|
"Status": "PENDING",
|
||||||
|
"Gender": "Not provided",
|
||||||
|
"YOB": "Not provided",
|
||||||
|
"Location": "Not provided",
|
||||||
|
"Offerings": "Not provided",
|
||||||
|
"FirstName": "Not provided",
|
||||||
|
"FamilyName": "Not provided",
|
||||||
|
},
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedData: map[string]string{
|
||||||
|
"TrackingId": "test-tracking-id",
|
||||||
|
"PublicKey": "test-public-key",
|
||||||
|
"CustodialId": "test-custodial-id",
|
||||||
|
"Status": "PENDING",
|
||||||
|
"Gender": "Not provided",
|
||||||
|
"YOB": "Not provided",
|
||||||
|
"Location": "Not provided",
|
||||||
|
"Offerings": "Not provided",
|
||||||
|
"FirstName": "Not provided",
|
||||||
|
"FamilyName": "Not provided",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the data file path using the session ID
|
||||||
|
accountFilePath := filepath.Join(tempDir, sessionID+"_data")
|
||||||
|
|
||||||
|
// Setup existing data if any
|
||||||
|
if tt.existingData != nil {
|
||||||
|
data, _ := json.Marshal(tt.existingData)
|
||||||
|
err := os.WriteFile(accountFilePath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write existing data: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function
|
||||||
|
result, err := h.CreateAccount(context.Background(), "", nil)
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateAccount returned an error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the result
|
||||||
|
if len(result.FlagSet) != len(tt.expectedResult.FlagSet) {
|
||||||
|
t.Errorf("Expected %d flags, got %d", len(tt.expectedResult.FlagSet), len(result.FlagSet))
|
||||||
|
}
|
||||||
|
for i, flag := range tt.expectedResult.FlagSet {
|
||||||
|
if result.FlagSet[i] != flag {
|
||||||
|
t.Errorf("Expected flag %d, got %d", flag, result.FlagSet[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the stored data
|
||||||
|
data, err := os.ReadFile(accountFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read account data file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedData map[string]string
|
||||||
|
err = json.Unmarshal(data, &storedData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal stored data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expectedValue := range tt.expectedData {
|
||||||
|
if storedValue, ok := storedData[key]; !ok || storedValue != expectedValue {
|
||||||
|
t.Errorf("Expected %s to be %s, got %s", key, expectedValue, storedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount_Success(t *testing.T) {
|
||||||
|
mockAccountFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
mockCreateAccountService := new(mocks.MockAccountService)
|
||||||
|
|
||||||
|
mockAccountFileHandler.On("EnsureFileExists").Return(nil)
|
||||||
|
|
||||||
|
// Mock that no account data exists
|
||||||
|
mockAccountFileHandler.On("ReadAccountData").Return(nil, nil)
|
||||||
|
|
||||||
|
// Define expected account response after api call
|
||||||
|
expectedAccountResp := &models.AccountResponse{
|
||||||
|
Ok: true,
|
||||||
|
Result: struct {
|
||||||
|
CustodialId json.Number `json:"custodialId"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
TrackingId string `json:"trackingId"`
|
||||||
|
}{
|
||||||
|
CustodialId: "12",
|
||||||
|
PublicKey: "0x8E0XSCSVA",
|
||||||
|
TrackingId: "d95a7e83-196c-4fd0-866fSGAGA",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil)
|
||||||
|
|
||||||
|
// Mock WriteAccountData to not error
|
||||||
|
mockAccountFileHandler.On("WriteAccountData", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
handlers := &Handlers{
|
||||||
|
accountService: mockCreateAccountService,
|
||||||
|
}
|
||||||
|
|
||||||
|
actualResponse, err := handlers.accountService.CreateAccount()
|
||||||
|
|
||||||
|
// Assert results
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedAccountResp.Ok, true)
|
||||||
|
assert.Equal(t, expectedAccountResp, actualResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavePin(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_save_pin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
sessionID := "07xxxxxxxx"
|
||||||
|
|
||||||
|
// Set up the data file path using the session ID
|
||||||
|
accountFilePath := filepath.Join(tempDir, sessionID+"_data")
|
||||||
|
initialAccountData := map[string]string{
|
||||||
|
"TrackingId": "test-tracking-id",
|
||||||
|
"PublicKey": "test-public-key",
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(initialAccountData)
|
||||||
|
err = os.WriteFile(accountFilePath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write initial account data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new AccountFileHandler and set it in the Handlers struct
|
||||||
|
accountFileHandler := utils.NewAccountFileHandler(accountFilePath)
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: accountFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
expectedFlags []uint32
|
||||||
|
expectedData map[string]string
|
||||||
|
expectedErrors bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN",
|
||||||
|
input: []byte("1234"),
|
||||||
|
expectedFlags: []uint32{},
|
||||||
|
expectedData: map[string]string{
|
||||||
|
"TrackingId": "test-tracking-id",
|
||||||
|
"PublicKey": "test-public-key",
|
||||||
|
"AccountPIN": "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN - non-numeric",
|
||||||
|
input: []byte("12ab"),
|
||||||
|
expectedFlags: []uint32{models.USERFLAG_INCORRECTPIN},
|
||||||
|
expectedData: initialAccountData, // No changes expected
|
||||||
|
expectedErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN - less than 4 digits",
|
||||||
|
input: []byte("123"),
|
||||||
|
expectedFlags: []uint32{models.USERFLAG_INCORRECTPIN},
|
||||||
|
expectedData: initialAccountData, // No changes expected
|
||||||
|
expectedErrors: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN - more than 4 digits",
|
||||||
|
input: []byte("12345"),
|
||||||
|
expectedFlags: []uint32{models.USERFLAG_INCORRECTPIN},
|
||||||
|
expectedData: initialAccountData, // No changes expected
|
||||||
|
expectedErrors: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Ensure the file exists before running the test
|
||||||
|
err := accountFileHandler.EnsureFileExists()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to ensure account file exists: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.SavePin(context.Background(), "", tt.input)
|
||||||
|
if err != nil && !tt.expectedErrors {
|
||||||
|
t.Fatalf("SavePin returned an unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.FlagSet) != len(tt.expectedFlags) {
|
||||||
|
t.Errorf("Expected %d flags, got %d", len(tt.expectedFlags), len(result.FlagSet))
|
||||||
|
}
|
||||||
|
for i, flag := range tt.expectedFlags {
|
||||||
|
if result.FlagSet[i] != flag {
|
||||||
|
t.Errorf("Expected flag %d, got %d", flag, result.FlagSet[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(accountFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read account data file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedData map[string]string
|
||||||
|
err = json.Unmarshal(data, &storedData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal stored data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expectedValue := range tt.expectedData {
|
||||||
|
if storedValue, ok := storedData[key]; !ok || storedValue != expectedValue {
|
||||||
|
t.Errorf("Expected %s to be %s, got %s", key, expectedValue, storedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveLocation(t *testing.T) {
|
||||||
|
// Create a new instance of MockAccountFileHandler
|
||||||
|
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
existingData map[string]string
|
||||||
|
writeError error
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful Save",
|
||||||
|
input: []byte("Mombasa"),
|
||||||
|
existingData: map[string]string{"Location": "Mombasa"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty location input",
|
||||||
|
input: []byte{},
|
||||||
|
existingData: map[string]string{"OtherKey": "OtherValue"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the mock expectations
|
||||||
|
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
||||||
|
if tt.expectedError == nil && len(tt.input) > 0 {
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["Location"] == string(tt.input)
|
||||||
|
})).Return(tt.writeError)
|
||||||
|
} else if len(tt.input) == 0 {
|
||||||
|
// For empty input, no WriteAccountData call should be made
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Handlers instance with the mock file handler
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Save Location
|
||||||
|
result, err := h.SaveLocation(context.Background(), "save_location", tt.input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save location with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
savedData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err == nil {
|
||||||
|
//Assert that the input provided is what was saved into the file
|
||||||
|
assert.Equal(t, string(tt.input), savedData["Location"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the results
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Equal(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
// Assert all expectations were met
|
||||||
|
mockFileHandler.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveFirstname(t *testing.T) {
|
||||||
|
// Create a new instance of MockAccountFileHandler
|
||||||
|
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
existingData map[string]string
|
||||||
|
writeError error
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful Save",
|
||||||
|
input: []byte("Joe"),
|
||||||
|
existingData: map[string]string{"Name": "Joe"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Input",
|
||||||
|
input: []byte{},
|
||||||
|
existingData: map[string]string{"OtherKey": "OtherValue"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the mock expectations
|
||||||
|
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
||||||
|
if tt.expectedError == nil && len(tt.input) > 0 {
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["FirstName"] == string(tt.input)
|
||||||
|
})).Return(tt.writeError)
|
||||||
|
} else if len(tt.input) == 0 {
|
||||||
|
// For empty input, no WriteAccountData call should be made
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Handlers instance with the mock file handler
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call save location
|
||||||
|
result, err := h.SaveFirstname(context.Background(), "save_location", tt.input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save first name with error: %v", err)
|
||||||
|
}
|
||||||
|
savedData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err == nil {
|
||||||
|
//Assert that the input provided is what was saved into the file
|
||||||
|
assert.Equal(t, string(tt.input), savedData["FirstName"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the results
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Equal(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
// Assert all expectations were met
|
||||||
|
mockFileHandler.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveFamilyName(t *testing.T) {
|
||||||
|
// Create a new instance of MockAccountFileHandler
|
||||||
|
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
existingData map[string]string
|
||||||
|
writeError error
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful Save",
|
||||||
|
input: []byte("Doe"),
|
||||||
|
existingData: map[string]string{"FamilyName": "Doe"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Input",
|
||||||
|
input: []byte{},
|
||||||
|
existingData: map[string]string{"FamilyName": "Doe"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the mock expectations
|
||||||
|
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
||||||
|
if tt.expectedError == nil && len(tt.input) > 0 {
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["FamilyName"] == string(tt.input)
|
||||||
|
})).Return(tt.writeError)
|
||||||
|
} else if len(tt.input) == 0 {
|
||||||
|
// For empty input, no WriteAccountData call should be made
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Handlers instance with the mock file handler
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call save familyname
|
||||||
|
result, err := h.SaveFamilyname(context.Background(), "save_familyname", tt.input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save family name with error: %v", err)
|
||||||
|
}
|
||||||
|
savedData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err == nil {
|
||||||
|
//Assert that the input provided is what was saved into the file
|
||||||
|
assert.Equal(t, string(tt.input), savedData["FamilyName"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the results
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Equal(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
// Assert all expectations were met
|
||||||
|
mockFileHandler.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveYOB(t *testing.T) {
|
||||||
|
// Create a new instance of MockAccountFileHandler
|
||||||
|
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
existingData map[string]string
|
||||||
|
writeError error
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful Save",
|
||||||
|
input: []byte("2006"),
|
||||||
|
existingData: map[string]string{"": ""},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "YOB less than 4 digits(invalid date entry)",
|
||||||
|
input: []byte{},
|
||||||
|
existingData: map[string]string{"": ""},
|
||||||
|
writeError: nil,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the mock expectations
|
||||||
|
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
||||||
|
if tt.expectedError == nil && len(tt.input) > 0 {
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["YOB"] == string(tt.input)
|
||||||
|
})).Return(tt.writeError)
|
||||||
|
} else if len(tt.input) != 4 {
|
||||||
|
// For input whose input is not a valid yob, no WriteAccountData call should be made
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Handlers instance with the mock file handler
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call save yob
|
||||||
|
result, err := h.SaveYob(context.Background(), "save_yob", tt.input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save family name with error: %v", err)
|
||||||
|
}
|
||||||
|
savedData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err == nil {
|
||||||
|
//Assert that the input provided is what was saved into the file
|
||||||
|
assert.Equal(t, string(tt.input), savedData["YOB"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the results
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Equal(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
// Assert all expectations were met
|
||||||
|
mockFileHandler.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveOfferings(t *testing.T) {
|
||||||
|
// Create a new instance of MockAccountFileHandler
|
||||||
|
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
existingData map[string]string
|
||||||
|
writeError error
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful Save",
|
||||||
|
input: []byte("Bananas"),
|
||||||
|
existingData: map[string]string{"": ""},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty input",
|
||||||
|
input: []byte{},
|
||||||
|
existingData: map[string]string{"": ""},
|
||||||
|
writeError: nil,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the mock expectations
|
||||||
|
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
||||||
|
if tt.expectedError == nil && len(tt.input) > 0 {
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["Offerings"] == string(tt.input)
|
||||||
|
})).Return(tt.writeError)
|
||||||
|
} else if len(tt.input) != 4 {
|
||||||
|
// For input whose input is not a valid yob, no WriteAccountData call should be made
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Handlers instance with the mock file handler
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call save yob
|
||||||
|
result, err := h.SaveOfferings(context.Background(), "save_offerings", tt.input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save offerings with error: %v", err)
|
||||||
|
}
|
||||||
|
savedData, err := h.accountFileHandler.ReadAccountData()
|
||||||
|
if err == nil {
|
||||||
|
//Assert that the input provided is what was saved into the file
|
||||||
|
assert.Equal(t, string(tt.input), savedData["Offerings"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the results
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Equal(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
// Assert all expectations were met
|
||||||
|
mockFileHandler.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestSaveGender(t *testing.T) {
|
||||||
|
// Create a new instance of MockAccountFileHandler
|
||||||
|
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
existingData map[string]string
|
||||||
|
writeError error
|
||||||
|
expectedResult resource.Result
|
||||||
|
expectedError error
|
||||||
|
expectedGender string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful Save - Male",
|
||||||
|
input: []byte("1"),
|
||||||
|
existingData: map[string]string{"OtherKey": "OtherValue"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedGender: "Male",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Successful Save - Female",
|
||||||
|
input: []byte("2"),
|
||||||
|
existingData: map[string]string{"OtherKey": "OtherValue"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedGender: "Female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Successful Save - Unspecified",
|
||||||
|
input: []byte("3"),
|
||||||
|
existingData: map[string]string{"OtherKey": "OtherValue"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedGender: "Unspecified",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Empty Input",
|
||||||
|
input: []byte{},
|
||||||
|
existingData: map[string]string{"OtherKey": "OtherValue"},
|
||||||
|
writeError: nil,
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedGender: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Set up the mock expectations
|
||||||
|
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
||||||
|
if tt.expectedError == nil && len(tt.input) > 0 {
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["Gender"] == tt.expectedGender
|
||||||
|
})).Return(tt.writeError)
|
||||||
|
} else if len(tt.input) == 0 {
|
||||||
|
// For empty input, no WriteAccountData call should be made
|
||||||
|
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Handlers instance with the mock file handler
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the method
|
||||||
|
result, err := h.SaveGender(context.Background(), "save_gender", tt.input)
|
||||||
|
|
||||||
|
// Assert the results
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Equal(t, tt.expectedError, err)
|
||||||
|
|
||||||
|
// Verify WriteAccountData was called with the expected data
|
||||||
|
if len(tt.input) > 0 && tt.expectedError == nil {
|
||||||
|
mockFileHandler.AssertCalled(t, "WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
||||||
|
return data["Gender"] == tt.expectedGender
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert all expectations were met
|
||||||
|
mockFileHandler.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSender(t *testing.T) {
|
||||||
|
mockAccountFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockAccountFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expectedResult resource.Result
|
||||||
|
accountData map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid public key",
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
Content: "test-public-key",
|
||||||
|
},
|
||||||
|
accountData: map[string]string{
|
||||||
|
"PublicKey": "test-public-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing public key",
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
Content: "",
|
||||||
|
},
|
||||||
|
accountData: map[string]string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset the mock state
|
||||||
|
mockAccountFileHandler.Mock = mock.Mock{}
|
||||||
|
|
||||||
|
mockAccountFileHandler.On("ReadAccountData").Return(tt.accountData, nil)
|
||||||
|
|
||||||
|
result, err := h.GetSender(context.Background(), "", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error occurred: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedResult.Content, result.Content)
|
||||||
|
mockAccountFileHandler.AssertCalled(t, "ReadAccountData")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAmount(t *testing.T) {
|
||||||
|
mockAccountFileHandler := new(mocks.MockAccountFileHandler)
|
||||||
|
h := &Handlers{
|
||||||
|
accountFileHandler: mockAccountFileHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expectedResult resource.Result
|
||||||
|
accountData map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid amount",
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
Content: "0.003",
|
||||||
|
},
|
||||||
|
accountData: map[string]string{
|
||||||
|
"Amount": "0.003",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing amount",
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
accountData: map[string]string{
|
||||||
|
"Amount": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset the mock state
|
||||||
|
mockAccountFileHandler.Mock = mock.Mock{}
|
||||||
|
|
||||||
|
mockAccountFileHandler.On("ReadAccountData").Return(tt.accountData, nil)
|
||||||
|
|
||||||
|
result, err := h.GetAmount(context.Background(), "", nil)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expectedResult.Content, result.Content)
|
||||||
|
|
||||||
|
mockAccountFileHandler.AssertCalled(t, "ReadAccountData")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
internal/handlers/ussd/mocks/mocks.go
Normal file
44
internal/handlers/ussd/mocks/mocks.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/models"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockAccountFileHandler struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountFileHandler) EnsureFileExists() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountFileHandler) ReadAccountData() (map[string]string, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(map[string]string), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountFileHandler) WriteAccountData(data map[string]string) error {
|
||||||
|
args := m.Called(data)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockAccountService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*models.AccountResponse), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckAccountStatus(TrackingId string) (string, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(string), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckBalance(PublicKey string) (string, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(string), args.Error(1)
|
||||||
|
}
|
15
internal/models/accountresponse.go
Normal file
15
internal/models/accountresponse.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Result struct {
|
||||||
|
CustodialId json.Number `json:"custodialId"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
TrackingId string `json:"trackingId"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
12
internal/models/balanceresponse.go
Normal file
12
internal/models/balanceresponse.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
|
||||||
|
type BalanceResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Result struct {
|
||||||
|
Balance string `json:"balance"`
|
||||||
|
Nonce json.Number `json:"nonce"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
22
internal/models/flags.go
Normal file
22
internal/models/flags.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "git.defalsify.org/vise.git/state"
|
||||||
|
|
||||||
|
const (
|
||||||
|
USERFLAG_LANGUAGE_SET = iota + state.FLAG_USERSTART
|
||||||
|
USERFLAG_ACCOUNT_CREATED
|
||||||
|
USERFLAG_ACCOUNT_PENDING
|
||||||
|
USERFLAG_ACCOUNT_SUCCESS
|
||||||
|
USERFLAG_ACCOUNT_AUTHORIZED
|
||||||
|
USERFLAG_INVALID_RECIPIENT
|
||||||
|
USERFLAG_INVALID_RECIPIENT_WITH_INVITE
|
||||||
|
USERFLAG_INCORRECTPIN
|
||||||
|
USERFLAG_ALLOW_UPDATE
|
||||||
|
USERFLAG_INVALID_AMOUNT
|
||||||
|
USERFLAG_PIN_SET
|
||||||
|
USERFLAG_VALIDPIN
|
||||||
|
USERFLAG_PINMISMATCH
|
||||||
|
USERFLAG_INCORRECTDATEFORMAT
|
||||||
|
USERFLAG_ACCOUNT_CREATION_FAILED
|
||||||
|
USERFLAG_SINGLE_EDIT
|
||||||
|
)
|
20
internal/models/trackstatusresponse.go
Normal file
20
internal/models/trackstatusresponse.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type TrackStatusResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Result struct {
|
||||||
|
Transaction struct {
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TransferValue json.Number `json:"transferValue"`
|
||||||
|
TxHash string `json:"txHash"`
|
||||||
|
TxType string `json:"txType"`
|
||||||
|
}
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
46
internal/utils/account_utils.go
Normal file
46
internal/utils/account_utils.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountFileHandler struct {
|
||||||
|
FilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountFileHandler(path string) *AccountFileHandler {
|
||||||
|
return &AccountFileHandler{FilePath: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (afh *AccountFileHandler) ReadAccountData() (map[string]string, error) {
|
||||||
|
jsonData, err := os.ReadFile(afh.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountData map[string]string
|
||||||
|
err = json.Unmarshal(jsonData, &accountData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (afh *AccountFileHandler) WriteAccountData(accountData map[string]string) error {
|
||||||
|
jsonData, err := json.Marshal(accountData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(afh.FilePath, jsonData, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (afh *AccountFileHandler) EnsureFileExists() error {
|
||||||
|
f, err := os.OpenFile(afh.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
35
internal/utils/age.go
Normal file
35
internal/utils/age.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
|
||||||
|
// It adjusts for cases where the current date is before the birthday in the current year.
|
||||||
|
func CalculateAge(birthdate, today time.Time) int {
|
||||||
|
today = today.In(birthdate.Location())
|
||||||
|
ty, tm, td := today.Date()
|
||||||
|
today = time.Date(ty, tm, td, 0, 0, 0, 0, time.UTC)
|
||||||
|
by, bm, bd := birthdate.Date()
|
||||||
|
birthdate = time.Date(by, bm, bd, 0, 0, 0, 0, time.UTC)
|
||||||
|
if today.Before(birthdate) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
age := ty - by
|
||||||
|
anniversary := birthdate.AddDate(age, 0, 0)
|
||||||
|
if anniversary.After(today) {
|
||||||
|
age--
|
||||||
|
}
|
||||||
|
return age
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateAgeWithYOB calculates the age based on the given year of birth (YOB).
|
||||||
|
// It subtracts the YOB from the current year to determine the age.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// yob: The year of birth as an integer.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// The calculated age as an integer.
|
||||||
|
func CalculateAgeWithYOB(yob int) int {
|
||||||
|
currentYear := time.Now().Year()
|
||||||
|
return currentYear - yob
|
||||||
|
}
|
13
internal/utils/filehandler.go
Normal file
13
internal/utils/filehandler.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type AccountFileHandlerInterface interface {
|
||||||
|
EnsureFileExists() error
|
||||||
|
ReadAccountData() (map[string]string, error)
|
||||||
|
WriteAccountData(data map[string]string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
17
services/registration/Makefile
Normal file
17
services/registration/Makefile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Variables to match files in the current directory
|
||||||
|
INPUTS = $(wildcard ./*.vis)
|
||||||
|
TXTS = $(wildcard ./*.txt.orig)
|
||||||
|
|
||||||
|
# Rule to build .bin files from .vis files
|
||||||
|
%.vis:
|
||||||
|
go run ../../go-vise/dev/asm $(basename $@).vis > $(basename $@).bin
|
||||||
|
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
||||||
|
|
||||||
|
# Rule to copy .orig files to .txt
|
||||||
|
%.txt.orig:
|
||||||
|
cp -v $(basename $@).orig $(basename $@)
|
||||||
|
@echo "Copied $(basename $@).orig to $(basename $@)"
|
||||||
|
|
||||||
|
# 'all' target depends on all .vis and .txt.orig files
|
||||||
|
all: $(INPUTS) $(TXTS)
|
||||||
|
@echo "Running all: $(INPUTS) $(TXTS)"
|
1
services/registration/account_creation
Normal file
1
services/registration/account_creation
Normal file
@ -0,0 +1 @@
|
|||||||
|
Your account is being created...
|
4
services/registration/account_creation.vis
Normal file
4
services/registration/account_creation.vis
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
RELOAD verify_pin
|
||||||
|
CATCH create_pin_mismatch 20 1
|
||||||
|
LOAD quit 0
|
||||||
|
HALT
|
1
services/registration/account_creation_failed
Normal file
1
services/registration/account_creation_failed
Normal file
@ -0,0 +1 @@
|
|||||||
|
Your account creation request failed. Please try again later.
|
3
services/registration/account_creation_failed.vis
Normal file
3
services/registration/account_creation_failed.vis
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
MOUT quit 9
|
||||||
|
HALT
|
||||||
|
INCMP quit 9
|
1
services/registration/account_creation_failed_swa
Normal file
1
services/registration/account_creation_failed_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
|
1
services/registration/account_creation_swa
Normal file
1
services/registration/account_creation_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Akaunti yako inatengenezwa...
|
1
services/registration/account_menu
Normal file
1
services/registration/account_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
My Account
|
1
services/registration/account_menu_swa
Normal file
1
services/registration/account_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Akaunti yangu
|
1
services/registration/account_pending
Normal file
1
services/registration/account_pending
Normal file
@ -0,0 +1 @@
|
|||||||
|
Your account is still being created.
|
3
services/registration/account_pending.vis
Normal file
3
services/registration/account_pending.vis
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
RELOAD check_account_status
|
||||||
|
CATCH main 11 1
|
||||||
|
HALT
|
1
services/registration/account_pending_swa
Normal file
1
services/registration/account_pending_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Akaunti yako bado inatengenezwa
|
1
services/registration/address
Normal file
1
services/registration/address
Normal file
@ -0,0 +1 @@
|
|||||||
|
Address: {{.check_identifier}}
|
6
services/registration/address.vis
Normal file
6
services/registration/address.vis
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
LOAD check_identifier 0
|
||||||
|
RELOAD check_identifier
|
||||||
|
MAP check_identifier
|
||||||
|
MOUT quit 9
|
||||||
|
HALT
|
||||||
|
INCMP quit 9
|
2
services/registration/amount
Normal file
2
services/registration/amount
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Maximum amount: {{.max_amount}}
|
||||||
|
Enter amount:
|
12
services/registration/amount.vis
Normal file
12
services/registration/amount.vis
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
LOAD reset_transaction_amount 0
|
||||||
|
LOAD max_amount 10
|
||||||
|
MAP max_amount
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
LOAD validate_amount 64
|
||||||
|
RELOAD validate_amount
|
||||||
|
CATCH invalid_amount 17 1
|
||||||
|
INCMP _ 0
|
||||||
|
LOAD get_recipient 12
|
||||||
|
LOAD get_sender 64
|
||||||
|
INCMP transaction_pin *
|
2
services/registration/amount_swa
Normal file
2
services/registration/amount_swa
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Kiwango cha juu: {{.max_amount}}
|
||||||
|
Weka kiwango:
|
1
services/registration/back_menu
Normal file
1
services/registration/back_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Back
|
1
services/registration/back_menu_swa
Normal file
1
services/registration/back_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Rudi
|
1
services/registration/balances
Normal file
1
services/registration/balances
Normal file
@ -0,0 +1 @@
|
|||||||
|
Balances:
|
8
services/registration/balances.vis
Normal file
8
services/registration/balances.vis
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
LOAD reset_unlocked 0
|
||||||
|
MOUT my_balance 1
|
||||||
|
MOUT community_balance 2
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
INCMP my_balance 1
|
||||||
|
INCMP community_balance 2
|
1
services/registration/balances_swa
Normal file
1
services/registration/balances_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Salio
|
1
services/registration/change_language_menu
Normal file
1
services/registration/change_language_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Change language
|
1
services/registration/change_language_menu_swa
Normal file
1
services/registration/change_language_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Badili lugha
|
1
services/registration/change_pin_menu
Normal file
1
services/registration/change_pin_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Change PIN
|
1
services/registration/change_pin_menu_swa
Normal file
1
services/registration/change_pin_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Badili PIN
|
1
services/registration/check_balance_menu
Normal file
1
services/registration/check_balance_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Check balances
|
1
services/registration/check_balance_menu_swa
Normal file
1
services/registration/check_balance_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Angalia salio
|
1
services/registration/check_statement_menu
Normal file
1
services/registration/check_statement_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Check statement
|
1
services/registration/check_statement_menu_swa
Normal file
1
services/registration/check_statement_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Taarifa ya matumizi
|
1
services/registration/comminity_balance_swa
Normal file
1
services/registration/comminity_balance_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Salio la kikundi
|
2
services/registration/community_balance
Normal file
2
services/registration/community_balance
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Your community balance is: 0.00SRF
|
||||||
|
|
5
services/registration/community_balance.vis
Normal file
5
services/registration/community_balance.vis
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
LOAD reset_incorrect 0
|
||||||
|
CATCH incorrect_pin 15 1
|
||||||
|
CATCH pin_entry 12 0
|
||||||
|
LOAD quit_with_balance 0
|
||||||
|
HALT
|
1
services/registration/community_balance_menu
Normal file
1
services/registration/community_balance_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Community balance
|
1
services/registration/community_balance_menu_swa
Normal file
1
services/registration/community_balance_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Salio la kikundi
|
1
services/registration/confirm_create_pin
Normal file
1
services/registration/confirm_create_pin
Normal file
@ -0,0 +1 @@
|
|||||||
|
Enter your four number PIN again:
|
4
services/registration/confirm_create_pin.vis
Normal file
4
services/registration/confirm_create_pin.vis
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
LOAD save_pin 0
|
||||||
|
HALT
|
||||||
|
LOAD verify_pin 8
|
||||||
|
INCMP account_creation *
|
1
services/registration/confirm_create_pin_swa
Normal file
1
services/registration/confirm_create_pin_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka PIN yako tena:
|
1
services/registration/create_pin
Normal file
1
services/registration/create_pin
Normal file
@ -0,0 +1 @@
|
|||||||
|
Please enter a new four number PIN for your account:
|
9
services/registration/create_pin.vis
Normal file
9
services/registration/create_pin.vis
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
LOAD create_account 0
|
||||||
|
CATCH account_creation_failed 22 1
|
||||||
|
MOUT exit 0
|
||||||
|
HALT
|
||||||
|
LOAD save_pin 0
|
||||||
|
RELOAD save_pin
|
||||||
|
CATCH . 15 1
|
||||||
|
INCMP quit 0
|
||||||
|
INCMP confirm_create_pin *
|
1
services/registration/create_pin_mismatch
Normal file
1
services/registration/create_pin_mismatch
Normal file
@ -0,0 +1 @@
|
|||||||
|
The PIN is not a match. Try again
|
5
services/registration/create_pin_mismatch.vis
Normal file
5
services/registration/create_pin_mismatch.vis
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
MOUT retry 1
|
||||||
|
MOUT quit 9
|
||||||
|
HALT
|
||||||
|
INCMP confirm_create_pin 1
|
||||||
|
INCMP quit 9
|
1
services/registration/create_pin_mismatch_swa
Normal file
1
services/registration/create_pin_mismatch_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
PIN uliyoweka haifanani. Jaribu tena
|
1
services/registration/create_pin_swa
Normal file
1
services/registration/create_pin_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Tafadhali weka PIN mpya yenye nambari nne kwa akaunti yako:
|
5
services/registration/display_profile_info
Normal file
5
services/registration/display_profile_info
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Wasifu wangu
|
||||||
|
Name: Not provided
|
||||||
|
Gender: Not provided
|
||||||
|
Age: Not provided
|
||||||
|
Location: Not provided
|
3
services/registration/display_profile_info.vis
Normal file
3
services/registration/display_profile_info.vis
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
0
services/registration/display_profile_info_swa
Normal file
0
services/registration/display_profile_info_swa
Normal file
1
services/registration/edit_gender_menu
Normal file
1
services/registration/edit_gender_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Edit gender
|
1
services/registration/edit_gender_menu_swa
Normal file
1
services/registration/edit_gender_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka jinsia
|
1
services/registration/edit_location_menu
Normal file
1
services/registration/edit_location_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Edit location
|
1
services/registration/edit_location_menu_swa
Normal file
1
services/registration/edit_location_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka eneo
|
1
services/registration/edit_name_menu
Normal file
1
services/registration/edit_name_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Edit name
|
1
services/registration/edit_name_menu_swa
Normal file
1
services/registration/edit_name_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka jina
|
1
services/registration/edit_offerings_menu
Normal file
1
services/registration/edit_offerings_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Edit offerings
|
1
services/registration/edit_offerings_menu_swa
Normal file
1
services/registration/edit_offerings_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka unachouza
|
1
services/registration/edit_profile
Normal file
1
services/registration/edit_profile
Normal file
@ -0,0 +1 @@
|
|||||||
|
My profile
|
20
services/registration/edit_profile.vis
Normal file
20
services/registration/edit_profile.vis
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
LOAD reset_account_authorized 16
|
||||||
|
LOAD reset_allow_update 0
|
||||||
|
RELOAD reset_allow_update
|
||||||
|
MOUT edit_name 1
|
||||||
|
MOUT edit_gender 2
|
||||||
|
MOUT edit_yob 3
|
||||||
|
MOUT edit_location 4
|
||||||
|
MOUT edit_offerings 5
|
||||||
|
MOUT view 6
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
LOAD set_reset_single_edit 0
|
||||||
|
RELOAD set_reset_single_edit
|
||||||
|
INCMP enter_name 1
|
||||||
|
INCMP select_gender 2
|
||||||
|
INCMP enter_yob 3
|
||||||
|
INCMP enter_location 4
|
||||||
|
INCMP enter_offerings 5
|
||||||
|
INCMP view_profile 6
|
1
services/registration/edit_profile_swa
Normal file
1
services/registration/edit_profile_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Wasifu wangu
|
1
services/registration/edit_yob_menu
Normal file
1
services/registration/edit_yob_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Edit year of birth
|
1
services/registration/edit_yob_menu_swa
Normal file
1
services/registration/edit_yob_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka mwaka wa kuzaliwa
|
1
services/registration/enter_familyname
Normal file
1
services/registration/enter_familyname
Normal file
@ -0,0 +1 @@
|
|||||||
|
Enter family name:
|
5
services/registration/enter_familyname.vis
Normal file
5
services/registration/enter_familyname.vis
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
LOAD save_firstname 0
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
INCMP select_gender *
|
0
services/registration/enter_familyname_swa
Normal file
0
services/registration/enter_familyname_swa
Normal file
1
services/registration/enter_location
Normal file
1
services/registration/enter_location
Normal file
@ -0,0 +1 @@
|
|||||||
|
Enter your location:
|
12
services/registration/enter_location.vis
Normal file
12
services/registration/enter_location.vis
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
CATCH incorrect_date_format 21 1
|
||||||
|
LOAD save_yob 0
|
||||||
|
CATCH update_success 16 1
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
LOAD save_location 0
|
||||||
|
CATCH pin_entry 23 1
|
||||||
|
INCMP enter_offerings *
|
||||||
|
|
||||||
|
|
||||||
|
|
1
services/registration/enter_location_swa
Normal file
1
services/registration/enter_location_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka eneo:
|
1
services/registration/enter_name
Normal file
1
services/registration/enter_name
Normal file
@ -0,0 +1 @@
|
|||||||
|
Enter your first names:
|
4
services/registration/enter_name.vis
Normal file
4
services/registration/enter_name.vis
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
INCMP enter_familyname *
|
1
services/registration/enter_name_swa
Normal file
1
services/registration/enter_name_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka majina yako ya kwanza:
|
1
services/registration/enter_offerings
Normal file
1
services/registration/enter_offerings
Normal file
@ -0,0 +1 @@
|
|||||||
|
Enter the services or goods you offer:
|
8
services/registration/enter_offerings.vis
Normal file
8
services/registration/enter_offerings.vis
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
LOAD save_location 0
|
||||||
|
CATCH incorrect_pin 15 1
|
||||||
|
CATCH update_success 16 1
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
LOAD save_offerings 0
|
||||||
|
INCMP _ 0
|
||||||
|
INCMP pin_entry *
|
1
services/registration/enter_offerings_swa
Normal file
1
services/registration/enter_offerings_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka unachouza
|
1
services/registration/enter_pin
Normal file
1
services/registration/enter_pin
Normal file
@ -0,0 +1 @@
|
|||||||
|
Please enter your PIN:
|
4
services/registration/enter_pin.vis
Normal file
4
services/registration/enter_pin.vis
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
INCMP display_profile_info *
|
1
services/registration/enter_pin_swa
Normal file
1
services/registration/enter_pin_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka PIN yako
|
1
services/registration/enter_yob
Normal file
1
services/registration/enter_yob
Normal file
@ -0,0 +1 @@
|
|||||||
|
Enter your year of birth
|
9
services/registration/enter_yob.vis
Normal file
9
services/registration/enter_yob.vis
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
LOAD save_gender 0
|
||||||
|
CATCH update_success 16 1
|
||||||
|
MOUT back 0
|
||||||
|
HALT
|
||||||
|
INCMP _ 0
|
||||||
|
LOAD verify_yob 8
|
||||||
|
LOAD save_yob 0
|
||||||
|
CATCH pin_entry 23 1
|
||||||
|
INCMP enter_location *
|
1
services/registration/enter_yob_swa
Normal file
1
services/registration/enter_yob_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Weka mwaka wa kuzaliwa
|
1
services/registration/exit_menu
Normal file
1
services/registration/exit_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Exit
|
1
services/registration/exit_menu_swa
Normal file
1
services/registration/exit_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Ondoka
|
1
services/registration/female_menu
Normal file
1
services/registration/female_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Female
|
1
services/registration/female_menu_swa
Normal file
1
services/registration/female_menu_swa
Normal file
@ -0,0 +1 @@
|
|||||||
|
Mwanamke
|
1
services/registration/guard_pin_menu
Normal file
1
services/registration/guard_pin_menu
Normal file
@ -0,0 +1 @@
|
|||||||
|
Guard my PIN
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user