wip-account-creation #4
@@ -1,2 +1,6 @@
|
||||
**/*.env
|
||||
covprofile
|
||||
covprofile
|
||||
go.work*
|
||||
**/*/*.bin
|
||||
**/*/.state/
|
||||
cmd/.state/
|
||||
|
||||
@@ -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
|
||||
|
lash marked this conversation as resolved
|
||||
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()
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
please rename this flag to USERFLAG_PINMISMATCH to avoid ambiguity. please rename this flag to USERFLAG_PINMISMATCH to avoid ambiguity.
lash
commented
priority **priority**
|
||||
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")
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
Please can we have all the http stuff in a separate package?
Please can we have all the http stuff in a separate package?
* urls
* responses
* gets
|
||||
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)
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
can this (and all its methods) be moved to a separate package please? can this (and all its methods) be moved to a separate package please?
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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,3 +1,5 @@
|
||||
module git.grassecon.net/urdt/ussd
|
||||
|
||||
go 1.22.6
|
||||
|
||||
require github.com/stretchr/testify v1.9.0 // indirect
|
||||
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
should not be necessary to set the flag when it already has been set and persisted. That will reduce one file access call each run. should not be necessary to set the flag when it already has been set and persisted. That will reduce one file access call each run.
|
||||
|
||||
// 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"])) {
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
I prefer putting regexes like this on top of the file as a const. I prefer putting regexes like this on top of the file as a const.
|
||||
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 {
|
||||
|
lash
commented
Please change to "Unspecified" Please change to "Unspecified"
|
||||
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) {
|
||||
|
lash
commented
"unlocked" risks being an ambiguous term when we introduce account blocking due to direct action or incorrect pin attempts. can we rename it please? "unlocked" risks being an ambiguous term when we introduce account blocking due to direct action or incorrect pin attempts. can we rename it please?
|
||||
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
|
||||
}
|
||||
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
were we using gettext here too? were we using gettext here too?
|
||||
// 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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
|
||||
|
||||
|
||||
type AccountFileHandlerInterface interface {
|
||||
EnsureFileExists() error
|
||||
ReadAccountData() (map[string]string, error)
|
||||
WriteAccountData(data map[string]string) error
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)"
|
||||
@@ -0,0 +1 @@
|
||||
Your account is being created...
|
||||
@@ -0,0 +1,4 @@
|
||||
RELOAD verify_pin
|
||||
CATCH create_pin_mismatch 20 1
|
||||
LOAD quit 0
|
||||
HALT
|
||||
@@ -0,0 +1 @@
|
||||
Your account creation request failed. Please try again later.
|
||||
@@ -0,0 +1,3 @@
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP quit 9
|
||||
@@ -0,0 +1 @@
|
||||
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
|
||||
@@ -0,0 +1 @@
|
||||
Akaunti yako inatengenezwa...
|
||||
@@ -0,0 +1 @@
|
||||
My Account
|
||||
@@ -0,0 +1 @@
|
||||
Akaunti yangu
|
||||
@@ -0,0 +1 @@
|
||||
Your account is still being created.
|
||||
@@ -0,0 +1,3 @@
|
||||
RELOAD check_account_status
|
||||
CATCH main 11 1
|
||||
HALT
|
||||
@@ -0,0 +1 @@
|
||||
Akaunti yako bado inatengenezwa
|
||||
@@ -0,0 +1 @@
|
||||
Address: {{.check_identifier}}
|
||||
@@ -0,0 +1,6 @@
|
||||
LOAD check_identifier 0
|
||||
RELOAD check_identifier
|
||||
MAP check_identifier
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP quit 9
|
||||
@@ -0,0 +1,2 @@
|
||||
Maximum amount: {{.max_amount}}
|
||||
Enter amount:
|
||||
@@ -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 *
|
||||
@@ -0,0 +1,2 @@
|
||||
Kiwango cha juu: {{.max_amount}}
|
||||
Weka kiwango:
|
||||
@@ -0,0 +1 @@
|
||||
Back
|
||||
@@ -0,0 +1 @@
|
||||
Rudi
|
||||
@@ -0,0 +1 @@
|
||||
Balances:
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Salio
|
||||
@@ -0,0 +1 @@
|
||||
Change language
|
||||
@@ -0,0 +1 @@
|
||||
Badili lugha
|
||||
@@ -0,0 +1 @@
|
||||
Change PIN
|
||||
@@ -0,0 +1 @@
|
||||
Badili PIN
|
||||
@@ -0,0 +1 @@
|
||||
Check balances
|
||||
@@ -0,0 +1 @@
|
||||
Angalia salio
|
||||
@@ -0,0 +1 @@
|
||||
Check statement
|
||||
@@ -0,0 +1 @@
|
||||
Taarifa ya matumizi
|
||||
@@ -0,0 +1 @@
|
||||
Salio la kikundi
|
||||
@@ -0,0 +1,2 @@
|
||||
Your community balance is: 0.00SRF
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
LOAD reset_incorrect 0
|
||||
CATCH incorrect_pin 15 1
|
||||
CATCH pin_entry 12 0
|
||||
LOAD quit_with_balance 0
|
||||
HALT
|
||||
@@ -0,0 +1 @@
|
||||
Community balance
|
||||
@@ -0,0 +1 @@
|
||||
Salio la kikundi
|
||||
@@ -0,0 +1 @@
|
||||
Enter your four number PIN again:
|
||||
@@ -0,0 +1,4 @@
|
||||
LOAD save_pin 0
|
||||
HALT
|
||||
LOAD verify_pin 8
|
||||
INCMP account_creation *
|
||||
@@ -0,0 +1 @@
|
||||
Weka PIN yako tena:
|
||||
@@ -0,0 +1 @@
|
||||
Please enter a new four number PIN for your account:
|
||||
@@ -0,0 +1,9 @@
|
||||
LOAD create_account 0
|
||||
CATCH account_creation_failed 22 1
|
||||
|
Alfred-mk marked this conversation as resolved
Outdated
lash
commented
This is not caught This is not caught
lash
commented
priority **priority**
|
||||
MOUT exit 0
|
||||
HALT
|
||||
LOAD save_pin 0
|
||||
RELOAD save_pin
|
||||
CATCH . 15 1
|
||||
INCMP quit 0
|
||||
INCMP confirm_create_pin *
|
||||
@@ -0,0 +1 @@
|
||||
The PIN is not a match. Try again
|
||||
@@ -0,0 +1,5 @@
|
||||
MOUT retry 1
|
||||
MOUT quit 9
|
||||
HALT
|
||||
INCMP confirm_create_pin 1
|
||||
INCMP quit 9
|
||||
|
Alfred-mk marked this conversation as resolved
lash
commented
if quit is chosen, next time the vm is started, the pin creation is never resumed, it just goes directly to main menu. It should prompt for setting pin again. if quit is chosen, next time the vm is started, the pin creation is never resumed, it just goes directly to main menu. It should prompt for setting pin again.
|
||||
@@ -0,0 +1 @@
|
||||
PIN uliyoweka haifanani. Jaribu tena
|
||||
@@ -0,0 +1 @@
|
||||
Tafadhali weka PIN mpya yenye nambari nne kwa akaunti yako:
|
||||
@@ -0,0 +1,5 @@
|
||||
Wasifu wangu
|
||||
Name: Not provided
|
||||
Gender: Not provided
|
||||
Age: Not provided
|
||||
Location: Not provided
|
||||
@@ -0,0 +1,3 @@
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
@@ -0,0 +1 @@
|
||||
Edit gender
|
||||
@@ -0,0 +1 @@
|
||||
Weka jinsia
|
||||
@@ -0,0 +1 @@
|
||||
Edit location
|
||||
@@ -0,0 +1 @@
|
||||
Weka eneo
|
||||
@@ -0,0 +1 @@
|
||||
Edit name
|
||||
@@ -0,0 +1 @@
|
||||
Weka jina
|
||||
@@ -0,0 +1 @@
|
||||
Edit offerings
|
||||
@@ -0,0 +1 @@
|
||||
Weka unachouza
|
||||
@@ -0,0 +1 @@
|
||||
My profile
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Wasifu wangu
|
||||
@@ -0,0 +1 @@
|
||||
Edit year of birth
|
||||
@@ -0,0 +1 @@
|
||||
Weka mwaka wa kuzaliwa
|
||||
@@ -0,0 +1 @@
|
||||
Enter family name:
|
||||
@@ -0,0 +1,5 @@
|
||||
LOAD save_firstname 0
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP select_gender *
|
||||
|
lash
commented
See https://git.grassecon.net/urdt/ussd/pulls/4/files#issuecomment-1194 No matter where you start in the menu, you always go ahead to the end. Again, if this is how current USSD behaves, we can keep this for now. But we should add a nice-to-have task to change that behavior to only edit full profile when not already edited. See https://git.grassecon.net/urdt/ussd/pulls/4/files#issuecomment-1194
No matter where you start in the menu, you always go ahead to the end.
Again, if this is how current USSD behaves, we can keep this for now. But we should add a nice-to-have task to change that behavior to only edit full profile when not already edited.
|
||||
@@ -0,0 +1 @@
|
||||
Enter your location:
|
||||
@@ -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 *
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Weka eneo:
|
||||
@@ -0,0 +1 @@
|
||||
Enter your first names:
|
||||
@@ -0,0 +1,4 @@
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP enter_familyname *
|
||||
|
lash
commented
Is this how it works on the USSD now? This has you fill out the entire profile at once. Is this how it works on the USSD now? This has you fill out the entire profile at once.
|
||||
@@ -0,0 +1 @@
|
||||
Weka majina yako ya kwanza:
|
||||
@@ -0,0 +1 @@
|
||||
Enter the services or goods you offer:
|
||||
@@ -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 *
|
||||
@@ -0,0 +1 @@
|
||||
Weka unachouza
|
||||
@@ -0,0 +1 @@
|
||||
Please enter your PIN:
|
||||
@@ -0,0 +1,4 @@
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
INCMP display_profile_info *
|
||||
@@ -0,0 +1 @@
|
||||
Weka PIN yako
|
||||
@@ -0,0 +1 @@
|
||||
Enter your year of birth
|
||||
@@ -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 *
|
||||
@@ -0,0 +1 @@
|
||||
Weka mwaka wa kuzaliwa
|
||||
@@ -0,0 +1 @@
|
||||
Exit
|
||||
@@ -0,0 +1 @@
|
||||
Ondoka
|
||||
@@ -0,0 +1 @@
|
||||
Female
|
||||
@@ -0,0 +1 @@
|
||||
Mwanamke
|
||||
@@ -0,0 +1 @@
|
||||
Guard my PIN
|
||||
Please add a documentation line on each.
priority