wip-account-creation #4
6
.gitignore
vendored
@ -1,2 +1,6 @@
|
||||
**/*.env
|
||||
covprofile
|
||||
covprofile
|
||||
go.work*
|
||||
**/*/*.bin
|
||||
**/*/.state/
|
||||
cmd/.state/
|
||||
|
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
|
||||
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)
|
||||
}
|
||||
}
|
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
@ -0,0 +1 @@
|
||||
Subproject commit 1f47a674d95380be8c387f410f0342eb72357df5
|
2
go.mod
@ -1,3 +1,5 @@
|
||||
module git.grassecon.net/urdt/ussd
|
||||
|
||||
go 1.22.6
|
||||
|
||||
require github.com/stretchr/testify v1.9.0 // indirect
|
||||
|
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
@ -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
@ -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
|
||||
}
|
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
Your account is being created...
|
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
@ -0,0 +1 @@
|
||||
Your account creation request failed. Please try again later.
|
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
@ -0,0 +1 @@
|
||||
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
|
1
services/registration/account_creation_swa
Normal file
@ -0,0 +1 @@
|
||||
Akaunti yako inatengenezwa...
|
1
services/registration/account_menu
Normal file
@ -0,0 +1 @@
|
||||
My Account
|
1
services/registration/account_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Akaunti yangu
|
1
services/registration/account_pending
Normal file
@ -0,0 +1 @@
|
||||
Your account is still being created.
|
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
@ -0,0 +1 @@
|
||||
Akaunti yako bado inatengenezwa
|
1
services/registration/address
Normal file
@ -0,0 +1 @@
|
||||
Address: {{.check_identifier}}
|
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
@ -0,0 +1,2 @@
|
||||
Maximum amount: {{.max_amount}}
|
||||
Enter amount:
|
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
@ -0,0 +1,2 @@
|
||||
Kiwango cha juu: {{.max_amount}}
|
||||
Weka kiwango:
|
1
services/registration/back_menu
Normal file
@ -0,0 +1 @@
|
||||
Back
|
1
services/registration/back_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Rudi
|
1
services/registration/balances
Normal file
@ -0,0 +1 @@
|
||||
Balances:
|
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
@ -0,0 +1 @@
|
||||
Salio
|
1
services/registration/change_language_menu
Normal file
@ -0,0 +1 @@
|
||||
Change language
|
1
services/registration/change_language_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Badili lugha
|
1
services/registration/change_pin_menu
Normal file
@ -0,0 +1 @@
|
||||
Change PIN
|
1
services/registration/change_pin_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Badili PIN
|
1
services/registration/check_balance_menu
Normal file
@ -0,0 +1 @@
|
||||
Check balances
|
1
services/registration/check_balance_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Angalia salio
|
1
services/registration/check_statement_menu
Normal file
@ -0,0 +1 @@
|
||||
Check statement
|
1
services/registration/check_statement_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Taarifa ya matumizi
|
1
services/registration/comminity_balance_swa
Normal file
@ -0,0 +1 @@
|
||||
Salio la kikundi
|
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
@ -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
@ -0,0 +1 @@
|
||||
Community balance
|
1
services/registration/community_balance_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Salio la kikundi
|
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
@ -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
@ -0,0 +1 @@
|
||||
Weka PIN yako tena:
|
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
@ -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 *
|
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
@ -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.
|
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
@ -0,0 +1 @@
|
||||
Tafadhali weka PIN mpya yenye nambari nne kwa akaunti yako:
|
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
@ -0,0 +1,3 @@
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
0
services/registration/display_profile_info_swa
Normal file
1
services/registration/edit_gender_menu
Normal file
@ -0,0 +1 @@
|
||||
Edit gender
|
1
services/registration/edit_gender_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Weka jinsia
|
1
services/registration/edit_location_menu
Normal file
@ -0,0 +1 @@
|
||||
Edit location
|
1
services/registration/edit_location_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Weka eneo
|
1
services/registration/edit_name_menu
Normal file
@ -0,0 +1 @@
|
||||
Edit name
|
1
services/registration/edit_name_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Weka jina
|
1
services/registration/edit_offerings_menu
Normal file
@ -0,0 +1 @@
|
||||
Edit offerings
|
1
services/registration/edit_offerings_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Weka unachouza
|
1
services/registration/edit_profile
Normal file
@ -0,0 +1 @@
|
||||
My profile
|
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
@ -0,0 +1 @@
|
||||
Wasifu wangu
|
1
services/registration/edit_yob_menu
Normal file
@ -0,0 +1 @@
|
||||
Edit year of birth
|
1
services/registration/edit_yob_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Weka mwaka wa kuzaliwa
|
1
services/registration/enter_familyname
Normal file
@ -0,0 +1 @@
|
||||
Enter family name:
|
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 *
|
||||
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
services/registration/enter_familyname_swa
Normal file
1
services/registration/enter_location
Normal file
@ -0,0 +1 @@
|
||||
Enter your location:
|
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
@ -0,0 +1 @@
|
||||
Weka eneo:
|
1
services/registration/enter_name
Normal file
@ -0,0 +1 @@
|
||||
Enter your first names:
|
4
services/registration/enter_name.vis
Normal file
@ -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.
|
1
services/registration/enter_name_swa
Normal file
@ -0,0 +1 @@
|
||||
Weka majina yako ya kwanza:
|
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
@ -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
@ -0,0 +1 @@
|
||||
Weka unachouza
|
1
services/registration/enter_pin
Normal file
@ -0,0 +1 @@
|
||||
Please enter your PIN:
|
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
@ -0,0 +1 @@
|
||||
Weka PIN yako
|
1
services/registration/enter_yob
Normal file
@ -0,0 +1 @@
|
||||
Enter your year of birth
|
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
@ -0,0 +1 @@
|
||||
Weka mwaka wa kuzaliwa
|
1
services/registration/exit_menu
Normal file
@ -0,0 +1 @@
|
||||
Exit
|
1
services/registration/exit_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Ondoka
|
1
services/registration/female_menu
Normal file
@ -0,0 +1 @@
|
||||
Female
|
1
services/registration/female_menu_swa
Normal file
@ -0,0 +1 @@
|
||||
Mwanamke
|
1
services/registration/guard_pin_menu
Normal file
@ -0,0 +1 @@
|
||||
Guard my PIN
|
Please add a documentation line on each.
priority