diff --git a/.gitignore b/.gitignore index 562e720..54ad3ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ **/*.env -covprofile \ No newline at end of file +covprofile +go.work* +**/*/*.bin +**/*/.state/ +cmd/.state/ diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..2daee8c --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "path" + + "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/persist" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" + "git.grassecon.net/urdt/ussd/internal/handlers/ussd" + "git.grassecon.net/urdt/ussd/internal/models" +) + +var ( + scriptDir = path.Join("services", "registration") +) + +func main() { + var dir string + var root string + var size uint + var sessionId string + flag.StringVar(&dir, "d", ".", "resource dir to read from") + flag.UintVar(&size, "s", 0, "max size of output") + flag.StringVar(&root, "root", "root", "entry point symbol") + flag.StringVar(&sessionId, "session-id", "default", "session id") + flag.Parse() + fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir) + + ctx := context.Background() + st := state.NewState(16) + st.UseDebug() + state.FlagDebugger.Register(models.USERFLAG_LANGUAGE_SET, "LANGUAGE_CHANGE") + state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATED, "ACCOUNT_CREATED") + state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_SUCCESS, "ACCOUNT_SUCCESS") + state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_PENDING, "ACCOUNT_PENDING") + state.FlagDebugger.Register(models.USERFLAG_INCORRECTPIN, "INCORRECTPIN") + state.FlagDebugger.Register(models.USERFLAG_INCORRECTDATEFORMAT, "INVALIDDATEFORMAT") + state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT, "INVALIDRECIPIENT") + state.FlagDebugger.Register(models.USERFLAG_PINMISMATCH, "PINMISMATCH") + state.FlagDebugger.Register(models.USERFLAG_PIN_SET, "PIN_SET") + state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE, "INVALIDRECIPIENT_WITH_INVITE") + state.FlagDebugger.Register(models.USERFLAG_INVALID_AMOUNT, "INVALIDAMOUNT") + state.FlagDebugger.Register(models.USERFLAG_ALLOW_UPDATE, "UNLOCKFORUPDATE") + state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "VALIDPIN") + state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "ACCOUNTUNLOCKED") + state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATION_FAILED, "ACCOUNT_CREATION_FAILED") + state.FlagDebugger.Register(models.USERFLAG_SINGLE_EDIT, "SINGLEEDIT") + + rfs := resource.NewFsResource(scriptDir) + ca := cache.NewCache() + cfg := engine.Config{ + Root: "root", + SessionId: sessionId, + } + + dp := path.Join(scriptDir, ".state") + err := os.MkdirAll(dp, 0700) + if err != nil { + fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err) + os.Exit(1) + } + pr := persist.NewFsPersister(dp) + en, err := engine.NewPersistedEngine(ctx, cfg, pr, rfs) + + if err != nil { + pr = pr.WithContent(&st, ca) + err = pr.Save(cfg.SessionId) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to save state with error: %v\n", err) + } + en, err = engine.NewPersistedEngine(ctx, cfg, pr, rfs) + if err != nil { + fmt.Fprintf(os.Stderr, "engine create exited with error: %v\n", err) + os.Exit(1) + } + } + + fp := path.Join(dp, sessionId) + + ussdHandlers := ussd.NewHandlers(fp, &st) + + rfs.AddLocalFunc("select_language", ussdHandlers.SetLanguage) + rfs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) + rfs.AddLocalFunc("save_pin", ussdHandlers.SavePin) + rfs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) + rfs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) + rfs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) + rfs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) + rfs.AddLocalFunc("quit", ussdHandlers.Quit) + rfs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) + rfs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) + rfs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) + rfs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) + rfs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) + rfs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) + rfs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) + rfs.AddLocalFunc("get_sender", ussdHandlers.GetSender) + rfs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) + rfs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) + rfs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) + rfs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) + rfs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) + rfs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) + rfs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) + rfs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) + rfs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) + rfs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) + rfs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) + rfs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) + rfs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) + rfs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) + rfs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) + rfs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) + + cont, err := en.Init(ctx) + en.SetDebugger(engine.NewSimpleDebug(nil)) + if err != nil { + fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err) + os.Exit(1) + } + if !cont { + _, err = en.WriteResult(ctx, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "dead init write error: %v\n", err) + os.Exit(1) + } + err = en.Finish() + if err != nil { + fmt.Fprintf(os.Stderr, "engine finish error: %v\n", err) + os.Exit(1) + } + os.Stdout.Write([]byte{0x0a}) + os.Exit(0) + } + err = engine.Loop(ctx, en, os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) + os.Exit(1) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..0571503 --- /dev/null +++ b/config/config.go @@ -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/" +) + diff --git a/go-vise b/go-vise new file mode 160000 index 0000000..1f47a67 --- /dev/null +++ b/go-vise @@ -0,0 +1 @@ +Subproject commit 1f47a674d95380be8c387f410f0342eb72357df5 diff --git a/go.mod b/go.mod index 8358c8c..de33912 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f203383 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go new file mode 100644 index 0000000..f4375a1 --- /dev/null +++ b/internal/handlers/server/accountservice.go @@ -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 +} diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go new file mode 100644 index 0000000..4eb2a83 --- /dev/null +++ b/internal/handlers/ussd/menuhandler.go @@ -0,0 +1,779 @@ +package ussd + +import ( + "bytes" + "context" + "fmt" + "path" + "regexp" + "strconv" + "strings" + + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/lang" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" + "git.grassecon.net/urdt/ussd/internal/handlers/server" + "git.grassecon.net/urdt/ussd/internal/models" + "git.grassecon.net/urdt/ussd/internal/utils" + "gopkg.in/leonelquinteros/gotext.v1" +) + +var ( + scriptDir = path.Join("services", "registration") + translationDir = path.Join(scriptDir, "locale") +) + +type FSData struct { + Path string + St *state.State +} + + +type Handlers struct { + fs *FSData + accountFileHandler utils.AccountFileHandlerInterface + accountService server.AccountServiceInterface +} + +func NewHandlers(path string, st *state.State) *Handlers { + return &Handlers{ + fs: &FSData{ + Path: path, + St: st, + }, + accountFileHandler: utils.NewAccountFileHandler(path + "_data"), + accountService: &server.AccountService{}, + } +} + + + +// Define the regex pattern as a constant +const pinPattern = `^\d{4}$` + +// isValidPIN checks whether the given input is a 4 digit number +func isValidPIN(pin string) bool { + match, _ := regexp.MatchString(pinPattern, pin) + return match +} + +// SetLanguage sets the language across the menu +func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { + inputStr := string(input) + res := resource.Result{} + switch inputStr { + case "0": + res.FlagSet = []uint32{state.FLAG_LANG} + res.Content = "eng" + case "1": + res.FlagSet = []uint32{state.FLAG_LANG} + res.Content = "swa" + default: + } + + res.FlagSet = append(res.FlagSet, models.USERFLAG_LANGUAGE_SET) + + return res, nil +} + +// CreateAccount checks if any account exists on the JSON data file, and if not +// creates an account on the API, +// sets the default values and flags +func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + err := h.accountFileHandler.EnsureFileExists() + if err != nil { + return res, err + } + + // if an account exists, return to prevent duplicate account creation + existingAccountData, err := h.accountFileHandler.ReadAccountData() + if existingAccountData != nil { + return res, err + } + + accountResp, err := h.accountService.CreateAccount() + if err != nil { + res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_CREATION_FAILED) + return res, err + } + + accountData := map[string]string{ + "TrackingId": accountResp.Result.TrackingId, + "PublicKey": accountResp.Result.PublicKey, + "CustodialId": accountResp.Result.CustodialId.String(), + "Status": "PENDING", + "Gender": "Not provided", + "YOB": "Not provided", + "Location": "Not provided", + "Offerings": "Not provided", + "FirstName": "Not provided", + "FamilyName": "Not provided", + } + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_CREATED) + return res, err +} + +// SavePin persists the user's PIN choice into the filesystem +func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + accountPIN := string(input) + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + // Validate that the PIN is a 4-digit number + if !isValidPIN(accountPIN) { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTPIN) + return res, nil + } + + res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN) + accountData["AccountPIN"] = accountPIN + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + return res, nil +} + +// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information. +func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + menuOption := string(input) + switch menuOption { + case "2": + res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE) + res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT) + case "3": + res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE) + res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT) + case "4": + res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE) + res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT) + default: + res.FlagReset = append(res.FlagReset, models.USERFLAG_SINGLE_EDIT) + } + + return res, nil +} + +// VerifyPin checks whether the confirmation PIN is similar to the account PIN +// If similar, it sets the USERFLAG_PIN_SET flag allowing the user +// to access the main menu +func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + if bytes.Equal(input, []byte(accountData["AccountPIN"])) { + res.FlagSet = []uint32{models.USERFLAG_VALIDPIN} + res.FlagReset = []uint32{models.USERFLAG_PINMISMATCH} + res.FlagSet = append(res.FlagSet, models.USERFLAG_PIN_SET) + } else { + res.FlagSet = []uint32{models.USERFLAG_PINMISMATCH} + } + + return res, nil +} + +//codeFromCtx retrieves language codes from the context that can be used for handling translations +func codeFromCtx(ctx context.Context) string { + var code string + engine.Logg.DebugCtxf(ctx, "in msg", "ctx", ctx, "val", code) + if ctx.Value("Language") != nil { + lang := ctx.Value("Language").(lang.Language) + code = lang.Code + } + return code +} + +// SaveFirstname updates the first name in a JSON data file with the provided input. +func (h *Handlers) SaveFirstname(cxt context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + if len(input) > 0 { + name := string(input) + accountData["FirstName"] = name + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + + return res, nil +} + +// SaveFamilyname updates the family name in a JSON data file with the provided input. +func (h *Handlers) SaveFamilyname(cxt context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + if len(input) > 0 { + secondname := string(input) + accountData["FamilyName"] = secondname + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + + return res, nil +} + +// SaveYOB updates the Year of Birth(YOB) in a JSON data file with the provided input. +func (h *Handlers) SaveYob(cxt context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + yob := string(input) + if len(yob) == 4 { + yob := string(input) + accountData["YOB"] = yob + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + + return res, nil +} + +// SaveLocation updates the location in a JSON data file with the provided input. +func (h *Handlers) SaveLocation(cxt context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + if len(input) > 0 { + location := string(input) + accountData["Location"] = location + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + + return res, nil +} + +// SaveGender updates the gender in a JSON data file with the provided input. +func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + if len(input) > 0 { + gender := string(input) + + switch gender { + case "1": + gender = "Male" + case "2": + gender = "Female" + case "3": + gender = "Unspecified" + } + accountData["Gender"] = gender + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + return res, nil +} + +// SaveOfferings updates the offerings(goods and services provided by the user) in a JSON data file with the provided input. +func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + if len(input) > 0 { + offerings := string(input) + accountData["Offerings"] = offerings + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + return res, nil +} + +// ResetAllowUpdate resets the allowupdate flag that allows a user to update profile data. +func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + res.FlagReset = append(res.FlagReset, models.USERFLAG_ALLOW_UPDATE) + return res, nil +} + +// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry. +func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED) + return res, nil +} + +// CheckIdentifier retrieves the PublicKey from the JSON data file. +func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + res.Content = accountData["PublicKey"] + + return res, nil +} + +// Authorize attempts to unlock the next sequential nodes by verifying the provided PIN against the already set PIN. +// It sets the required flags that control the flow. +func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + pin := string(input) + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + if len(input) == 4 { + if pin != accountData["AccountPIN"] { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTPIN) + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED) + return res, nil + } + if h.fs.St.MatchFlag(models.USERFLAG_ACCOUNT_AUTHORIZED, false) { + res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN) + res.FlagSet = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE) + res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_AUTHORIZED) + } else { + res.FlagSet = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE) + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED) + } + } + return res, nil +} + +// ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt. +func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN) + return res, nil +} + +// CheckAccountStatus queries the API using the TrackingId and sets flags +// based on the account status +func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + status, err := h.accountService.CheckAccountStatus(accountData["TrackingId"]) + + if err != nil { + fmt.Println("Error checking account status:", err) + return res, nil + } + + accountData["Status"] = status + + if status == "SUCCESS" { + res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_SUCCESS) + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_PENDING) + } else { + res.FlagReset = append(res.FlagSet, models.USERFLAG_ACCOUNT_SUCCESS) + res.FlagSet = append(res.FlagReset, models.USERFLAG_ACCOUNT_PENDING) + } + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + return res, nil +} + +// Quit displays the Thank you message and exits the menu +func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + + res.Content = l.Get("Thank you for using Sarafu. Goodbye!") + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED) + return res, nil +} + +// VerifyYob verifies the length of the given input +func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + date := string(input) + _, err := strconv.Atoi(date) + if err != nil { + // If conversion fails, input is not numeric + res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTDATEFORMAT) + return res, nil + } + + if len(date) == 4 { + res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTDATEFORMAT) + } else { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTDATEFORMAT) + } + + return res, nil +} + +// ResetIncorrectYob resets the incorrect date format after a new attempt +func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTDATEFORMAT) + return res, nil +} + +// CheckBalance retrieves the balance from the API using the "PublicKey" and sets +// the balance as the result content +func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + balance, err := h.accountService.CheckBalance(accountData["PublicKey"]) + if err != nil { + return res, nil + } + res.Content = balance + + return res, nil +} + +// ValidateRecipient validates that the given input is a valid phone number. +func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + recipient := string(input) + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + if recipient != "0" { + // mimic invalid number check + if recipient == "000" { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_RECIPIENT) + res.Content = recipient + + return res, nil + } + + accountData["Recipient"] = recipient + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + } + + return res, nil +} + +// TransactionReset resets the previous transaction data (Recipient and Amount) +// as well as the invalid flags +func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + // reset the transaction + accountData["Recipient"] = "" + accountData["Amount"] = "" + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + res.FlagReset = append(res.FlagReset, models.USERFLAG_INVALID_RECIPIENT, models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE) + + return res, nil +} + +// ResetTransactionAmount resets the transaction amount and invalid flag +func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + // reset the amount + accountData["Amount"] = "" + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + res.FlagReset = append(res.FlagReset, models.USERFLAG_INVALID_AMOUNT) + + return res, nil +} + +// MaxAmount gets the current balance from the API and sets it as +// the result content. +func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + balance, err := h.accountService.CheckBalance(accountData["PublicKey"]) + if err != nil { + return res, nil + } + + res.Content = balance + + return res, nil +} + +// ValidateAmount ensures that the given input is a valid amount and that +// it is not more than the current balance. +func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + amountStr := string(input) + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + balanceStr, err := h.accountService.CheckBalance(accountData["PublicKey"]) + if err != nil { + return res, err + } + res.Content = balanceStr + + // Parse the balance + balanceParts := strings.Split(balanceStr, " ") + if len(balanceParts) != 2 { + return res, fmt.Errorf("unexpected balance format: %s", balanceStr) + } + balanceValue, err := strconv.ParseFloat(balanceParts[0], 64) + if err != nil { + return res, fmt.Errorf("failed to parse balance: %v", err) + } + + // Extract numeric part from input + re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`) + matches := re.FindStringSubmatch(strings.TrimSpace(amountStr)) + if len(matches) < 2 { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT) + res.Content = amountStr + return res, nil + } + + inputAmount, err := strconv.ParseFloat(matches[1], 64) + if err != nil { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT) + res.Content = amountStr + return res, nil + } + + if inputAmount > balanceValue { + res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT) + res.Content = amountStr + return res, nil + } + + res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places + accountData["Amount"] = res.Content + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + return res, nil +} + +// GetRecipient returns the transaction recipient from a JSON data file. +func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + res.Content = accountData["Recipient"] + + return res, nil +} + +// GetProfileInfo retrieves and formats the profile information of a user from a JSON data file. +func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + var age string + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + var name string + if accountData["FirstName"] == "Not provided" || accountData["FamilyName"] == "Not provided" { + name = "Not provided" + } else { + name = accountData["FirstName"] + " " + accountData["FamilyName"] + } + + gender := accountData["Gender"] + yob := accountData["YOB"] + location := accountData["Location"] + offerings := accountData["Offerings"] + if yob == "Not provided" { + age = "Not provided" + } else { + ageInt, err := strconv.Atoi(yob) + if err != nil { + return res, nil + } + age = strconv.Itoa(utils.CalculateAgeWithYOB(ageInt)) + } + formattedData := fmt.Sprintf("Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", name, gender, age, location, offerings) + res.Content = formattedData + return res, nil +} + +// GetSender retrieves the public key from a JSON data file. +func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + res.Content = accountData["PublicKey"] + + return res, nil +} + +// GetAmount retrieves the amount from a JSON data file. +func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + res.Content = accountData["Amount"] + + return res, nil +} + +// QuickWithBalance retrieves the balance for a given public key from the custodial balance API endpoint before +// gracefully exiting the session. +func (h *Handlers) QuitWithBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + balance, err := h.accountService.CheckBalance(accountData["PublicKey"]) + if err != nil { + return res, nil + } + res.Content = l.Get("Your account balance is %s", balance) + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED) + return res, nil +} + +// InitiateTransaction returns a confirmation and resets the transaction data +// on the JSON file. +func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) { + res := resource.Result{} + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + + accountData, err := h.accountFileHandler.ReadAccountData() + if err != nil { + return res, err + } + + // TODO + // Use the amount, recipient and sender to call the API and initialize the transaction + + res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", accountData["Recipient"], accountData["Amount"], accountData["PublicKey"]) + + // reset the transaction + accountData["Recipient"] = "" + accountData["Amount"] = "" + + err = h.accountFileHandler.WriteAccountData(accountData) + if err != nil { + return res, err + } + + res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED) + return res, nil +} diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go new file mode 100644 index 0000000..5755092 --- /dev/null +++ b/internal/handlers/ussd/menuhandler_test.go @@ -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") + }) + } +} + diff --git a/internal/handlers/ussd/mocks/mocks.go b/internal/handlers/ussd/mocks/mocks.go new file mode 100644 index 0000000..3c7eb1e --- /dev/null +++ b/internal/handlers/ussd/mocks/mocks.go @@ -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) +} diff --git a/internal/models/accountresponse.go b/internal/models/accountresponse.go new file mode 100644 index 0000000..1422a20 --- /dev/null +++ b/internal/models/accountresponse.go @@ -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"` +} \ No newline at end of file diff --git a/internal/models/balanceresponse.go b/internal/models/balanceresponse.go new file mode 100644 index 0000000..57c8e5a --- /dev/null +++ b/internal/models/balanceresponse.go @@ -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"` +} diff --git a/internal/models/flags.go b/internal/models/flags.go new file mode 100644 index 0000000..ba6d617 --- /dev/null +++ b/internal/models/flags.go @@ -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 +) diff --git a/internal/models/trackstatusresponse.go b/internal/models/trackstatusresponse.go new file mode 100644 index 0000000..6054281 --- /dev/null +++ b/internal/models/trackstatusresponse.go @@ -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"` +} \ No newline at end of file diff --git a/internal/utils/account_utils.go b/internal/utils/account_utils.go new file mode 100644 index 0000000..39fb66b --- /dev/null +++ b/internal/utils/account_utils.go @@ -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() +} diff --git a/internal/utils/age.go b/internal/utils/age.go new file mode 100644 index 0000000..6b040e7 --- /dev/null +++ b/internal/utils/age.go @@ -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 +} \ No newline at end of file diff --git a/internal/utils/filehandler.go b/internal/utils/filehandler.go new file mode 100644 index 0000000..8b11e29 --- /dev/null +++ b/internal/utils/filehandler.go @@ -0,0 +1,13 @@ +package utils + + + + +type AccountFileHandlerInterface interface { + EnsureFileExists() error + ReadAccountData() (map[string]string, error) + WriteAccountData(data map[string]string) error +} + + + diff --git a/services/registration/Makefile b/services/registration/Makefile new file mode 100644 index 0000000..578ebc6 --- /dev/null +++ b/services/registration/Makefile @@ -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)" diff --git a/services/registration/account_creation b/services/registration/account_creation new file mode 100644 index 0000000..e9463a6 --- /dev/null +++ b/services/registration/account_creation @@ -0,0 +1 @@ +Your account is being created... \ No newline at end of file diff --git a/services/registration/account_creation.vis b/services/registration/account_creation.vis new file mode 100644 index 0000000..e3ecebb --- /dev/null +++ b/services/registration/account_creation.vis @@ -0,0 +1,4 @@ +RELOAD verify_pin +CATCH create_pin_mismatch 20 1 +LOAD quit 0 +HALT diff --git a/services/registration/account_creation_failed b/services/registration/account_creation_failed new file mode 100644 index 0000000..0df00db --- /dev/null +++ b/services/registration/account_creation_failed @@ -0,0 +1 @@ +Your account creation request failed. Please try again later. \ No newline at end of file diff --git a/services/registration/account_creation_failed.vis b/services/registration/account_creation_failed.vis new file mode 100644 index 0000000..b62b797 --- /dev/null +++ b/services/registration/account_creation_failed.vis @@ -0,0 +1,3 @@ +MOUT quit 9 +HALT +INCMP quit 9 diff --git a/services/registration/account_creation_failed_swa b/services/registration/account_creation_failed_swa new file mode 100644 index 0000000..6f0ac7b --- /dev/null +++ b/services/registration/account_creation_failed_swa @@ -0,0 +1 @@ +Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye. \ No newline at end of file diff --git a/services/registration/account_creation_swa b/services/registration/account_creation_swa new file mode 100644 index 0000000..6e5b1e1 --- /dev/null +++ b/services/registration/account_creation_swa @@ -0,0 +1 @@ +Akaunti yako inatengenezwa... \ No newline at end of file diff --git a/services/registration/account_menu b/services/registration/account_menu new file mode 100644 index 0000000..7aa9fe9 --- /dev/null +++ b/services/registration/account_menu @@ -0,0 +1 @@ +My Account \ No newline at end of file diff --git a/services/registration/account_menu_swa b/services/registration/account_menu_swa new file mode 100644 index 0000000..c77102f --- /dev/null +++ b/services/registration/account_menu_swa @@ -0,0 +1 @@ +Akaunti yangu \ No newline at end of file diff --git a/services/registration/account_pending b/services/registration/account_pending new file mode 100644 index 0000000..4eadf25 --- /dev/null +++ b/services/registration/account_pending @@ -0,0 +1 @@ +Your account is still being created. \ No newline at end of file diff --git a/services/registration/account_pending.vis b/services/registration/account_pending.vis new file mode 100644 index 0000000..19e308c --- /dev/null +++ b/services/registration/account_pending.vis @@ -0,0 +1,3 @@ +RELOAD check_account_status +CATCH main 11 1 +HALT diff --git a/services/registration/account_pending_swa b/services/registration/account_pending_swa new file mode 100644 index 0000000..2e514b5 --- /dev/null +++ b/services/registration/account_pending_swa @@ -0,0 +1 @@ +Akaunti yako bado inatengenezwa \ No newline at end of file diff --git a/services/registration/address b/services/registration/address new file mode 100644 index 0000000..6353876 --- /dev/null +++ b/services/registration/address @@ -0,0 +1 @@ +Address: {{.check_identifier}} \ No newline at end of file diff --git a/services/registration/address.vis b/services/registration/address.vis new file mode 100644 index 0000000..f3ba04a --- /dev/null +++ b/services/registration/address.vis @@ -0,0 +1,6 @@ +LOAD check_identifier 0 +RELOAD check_identifier +MAP check_identifier +MOUT quit 9 +HALT +INCMP quit 9 diff --git a/services/registration/amount b/services/registration/amount new file mode 100644 index 0000000..9142aba --- /dev/null +++ b/services/registration/amount @@ -0,0 +1,2 @@ +Maximum amount: {{.max_amount}} +Enter amount: \ No newline at end of file diff --git a/services/registration/amount.vis b/services/registration/amount.vis new file mode 100644 index 0000000..884c8a5 --- /dev/null +++ b/services/registration/amount.vis @@ -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 * diff --git a/services/registration/amount_swa b/services/registration/amount_swa new file mode 100644 index 0000000..0c8cf01 --- /dev/null +++ b/services/registration/amount_swa @@ -0,0 +1,2 @@ +Kiwango cha juu: {{.max_amount}} +Weka kiwango: \ No newline at end of file diff --git a/services/registration/back_menu b/services/registration/back_menu new file mode 100644 index 0000000..2278c97 --- /dev/null +++ b/services/registration/back_menu @@ -0,0 +1 @@ +Back \ No newline at end of file diff --git a/services/registration/back_menu_swa b/services/registration/back_menu_swa new file mode 100644 index 0000000..751fb22 --- /dev/null +++ b/services/registration/back_menu_swa @@ -0,0 +1 @@ +Rudi diff --git a/services/registration/balances b/services/registration/balances new file mode 100644 index 0000000..697be51 --- /dev/null +++ b/services/registration/balances @@ -0,0 +1 @@ +Balances: diff --git a/services/registration/balances.vis b/services/registration/balances.vis new file mode 100644 index 0000000..552acdb --- /dev/null +++ b/services/registration/balances.vis @@ -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 diff --git a/services/registration/balances_swa b/services/registration/balances_swa new file mode 100644 index 0000000..61a05a6 --- /dev/null +++ b/services/registration/balances_swa @@ -0,0 +1 @@ +Salio \ No newline at end of file diff --git a/services/registration/change_language_menu b/services/registration/change_language_menu new file mode 100644 index 0000000..7175fce --- /dev/null +++ b/services/registration/change_language_menu @@ -0,0 +1 @@ +Change language \ No newline at end of file diff --git a/services/registration/change_language_menu_swa b/services/registration/change_language_menu_swa new file mode 100644 index 0000000..02c46e1 --- /dev/null +++ b/services/registration/change_language_menu_swa @@ -0,0 +1 @@ +Badili lugha diff --git a/services/registration/change_pin_menu b/services/registration/change_pin_menu new file mode 100644 index 0000000..3ef432e --- /dev/null +++ b/services/registration/change_pin_menu @@ -0,0 +1 @@ +Change PIN diff --git a/services/registration/change_pin_menu_swa b/services/registration/change_pin_menu_swa new file mode 100644 index 0000000..cd48f9d --- /dev/null +++ b/services/registration/change_pin_menu_swa @@ -0,0 +1 @@ +Badili PIN diff --git a/services/registration/check_balance_menu b/services/registration/check_balance_menu new file mode 100644 index 0000000..253f368 --- /dev/null +++ b/services/registration/check_balance_menu @@ -0,0 +1 @@ +Check balances \ No newline at end of file diff --git a/services/registration/check_balance_menu_swa b/services/registration/check_balance_menu_swa new file mode 100644 index 0000000..4fe14f2 --- /dev/null +++ b/services/registration/check_balance_menu_swa @@ -0,0 +1 @@ +Angalia salio \ No newline at end of file diff --git a/services/registration/check_statement_menu b/services/registration/check_statement_menu new file mode 100644 index 0000000..f29634b --- /dev/null +++ b/services/registration/check_statement_menu @@ -0,0 +1 @@ +Check statement diff --git a/services/registration/check_statement_menu_swa b/services/registration/check_statement_menu_swa new file mode 100644 index 0000000..b8a338d --- /dev/null +++ b/services/registration/check_statement_menu_swa @@ -0,0 +1 @@ +Taarifa ya matumizi \ No newline at end of file diff --git a/services/registration/comminity_balance_swa b/services/registration/comminity_balance_swa new file mode 100644 index 0000000..726fc99 --- /dev/null +++ b/services/registration/comminity_balance_swa @@ -0,0 +1 @@ +Salio la kikundi \ No newline at end of file diff --git a/services/registration/community_balance b/services/registration/community_balance new file mode 100644 index 0000000..e79d40a --- /dev/null +++ b/services/registration/community_balance @@ -0,0 +1,2 @@ +Your community balance is: 0.00SRF + diff --git a/services/registration/community_balance.vis b/services/registration/community_balance.vis new file mode 100644 index 0000000..4894944 --- /dev/null +++ b/services/registration/community_balance.vis @@ -0,0 +1,5 @@ +LOAD reset_incorrect 0 +CATCH incorrect_pin 15 1 +CATCH pin_entry 12 0 +LOAD quit_with_balance 0 +HALT diff --git a/services/registration/community_balance_menu b/services/registration/community_balance_menu new file mode 100644 index 0000000..3833589 --- /dev/null +++ b/services/registration/community_balance_menu @@ -0,0 +1 @@ +Community balance \ No newline at end of file diff --git a/services/registration/community_balance_menu_swa b/services/registration/community_balance_menu_swa new file mode 100644 index 0000000..726fc99 --- /dev/null +++ b/services/registration/community_balance_menu_swa @@ -0,0 +1 @@ +Salio la kikundi \ No newline at end of file diff --git a/services/registration/confirm_create_pin b/services/registration/confirm_create_pin new file mode 100644 index 0000000..e4632ad --- /dev/null +++ b/services/registration/confirm_create_pin @@ -0,0 +1 @@ +Enter your four number PIN again: \ No newline at end of file diff --git a/services/registration/confirm_create_pin.vis b/services/registration/confirm_create_pin.vis new file mode 100644 index 0000000..1235916 --- /dev/null +++ b/services/registration/confirm_create_pin.vis @@ -0,0 +1,4 @@ +LOAD save_pin 0 +HALT +LOAD verify_pin 8 +INCMP account_creation * diff --git a/services/registration/confirm_create_pin_swa b/services/registration/confirm_create_pin_swa new file mode 100644 index 0000000..f697854 --- /dev/null +++ b/services/registration/confirm_create_pin_swa @@ -0,0 +1 @@ +Weka PIN yako tena: \ No newline at end of file diff --git a/services/registration/create_pin b/services/registration/create_pin new file mode 100644 index 0000000..f8836f5 --- /dev/null +++ b/services/registration/create_pin @@ -0,0 +1 @@ +Please enter a new four number PIN for your account: \ No newline at end of file diff --git a/services/registration/create_pin.vis b/services/registration/create_pin.vis new file mode 100644 index 0000000..4994863 --- /dev/null +++ b/services/registration/create_pin.vis @@ -0,0 +1,9 @@ +LOAD create_account 0 +CATCH account_creation_failed 22 1 +MOUT exit 0 +HALT +LOAD save_pin 0 +RELOAD save_pin +CATCH . 15 1 +INCMP quit 0 +INCMP confirm_create_pin * diff --git a/services/registration/create_pin_mismatch b/services/registration/create_pin_mismatch new file mode 100644 index 0000000..e75068c --- /dev/null +++ b/services/registration/create_pin_mismatch @@ -0,0 +1 @@ +The PIN is not a match. Try again \ No newline at end of file diff --git a/services/registration/create_pin_mismatch.vis b/services/registration/create_pin_mismatch.vis new file mode 100644 index 0000000..91793b5 --- /dev/null +++ b/services/registration/create_pin_mismatch.vis @@ -0,0 +1,5 @@ +MOUT retry 1 +MOUT quit 9 +HALT +INCMP confirm_create_pin 1 +INCMP quit 9 diff --git a/services/registration/create_pin_mismatch_swa b/services/registration/create_pin_mismatch_swa new file mode 100644 index 0000000..a1d7b6d --- /dev/null +++ b/services/registration/create_pin_mismatch_swa @@ -0,0 +1 @@ +PIN uliyoweka haifanani. Jaribu tena \ No newline at end of file diff --git a/services/registration/create_pin_swa b/services/registration/create_pin_swa new file mode 100644 index 0000000..1fdd972 --- /dev/null +++ b/services/registration/create_pin_swa @@ -0,0 +1 @@ +Tafadhali weka PIN mpya yenye nambari nne kwa akaunti yako: \ No newline at end of file diff --git a/services/registration/display_profile_info b/services/registration/display_profile_info new file mode 100644 index 0000000..669c6c6 --- /dev/null +++ b/services/registration/display_profile_info @@ -0,0 +1,5 @@ +Wasifu wangu +Name: Not provided +Gender: Not provided +Age: Not provided +Location: Not provided \ No newline at end of file diff --git a/services/registration/display_profile_info.vis b/services/registration/display_profile_info.vis new file mode 100644 index 0000000..3790a08 --- /dev/null +++ b/services/registration/display_profile_info.vis @@ -0,0 +1,3 @@ +MOUT back 0 +HALT +INCMP _ 0 diff --git a/services/registration/display_profile_info_swa b/services/registration/display_profile_info_swa new file mode 100644 index 0000000..e69de29 diff --git a/services/registration/edit_gender_menu b/services/registration/edit_gender_menu new file mode 100644 index 0000000..8946918 --- /dev/null +++ b/services/registration/edit_gender_menu @@ -0,0 +1 @@ +Edit gender \ No newline at end of file diff --git a/services/registration/edit_gender_menu_swa b/services/registration/edit_gender_menu_swa new file mode 100644 index 0000000..6d31ea8 --- /dev/null +++ b/services/registration/edit_gender_menu_swa @@ -0,0 +1 @@ +Weka jinsia \ No newline at end of file diff --git a/services/registration/edit_location_menu b/services/registration/edit_location_menu new file mode 100644 index 0000000..39ff1b7 --- /dev/null +++ b/services/registration/edit_location_menu @@ -0,0 +1 @@ +Edit location \ No newline at end of file diff --git a/services/registration/edit_location_menu_swa b/services/registration/edit_location_menu_swa new file mode 100644 index 0000000..a2a0e59 --- /dev/null +++ b/services/registration/edit_location_menu_swa @@ -0,0 +1 @@ +Weka eneo \ No newline at end of file diff --git a/services/registration/edit_name_menu b/services/registration/edit_name_menu new file mode 100644 index 0000000..63d97b9 --- /dev/null +++ b/services/registration/edit_name_menu @@ -0,0 +1 @@ +Edit name diff --git a/services/registration/edit_name_menu_swa b/services/registration/edit_name_menu_swa new file mode 100644 index 0000000..c50424f --- /dev/null +++ b/services/registration/edit_name_menu_swa @@ -0,0 +1 @@ +Weka jina diff --git a/services/registration/edit_offerings_menu b/services/registration/edit_offerings_menu new file mode 100644 index 0000000..74563b2 --- /dev/null +++ b/services/registration/edit_offerings_menu @@ -0,0 +1 @@ +Edit offerings diff --git a/services/registration/edit_offerings_menu_swa b/services/registration/edit_offerings_menu_swa new file mode 100644 index 0000000..f37e125 --- /dev/null +++ b/services/registration/edit_offerings_menu_swa @@ -0,0 +1 @@ +Weka unachouza \ No newline at end of file diff --git a/services/registration/edit_profile b/services/registration/edit_profile new file mode 100644 index 0000000..2c808e6 --- /dev/null +++ b/services/registration/edit_profile @@ -0,0 +1 @@ +My profile \ No newline at end of file diff --git a/services/registration/edit_profile.vis b/services/registration/edit_profile.vis new file mode 100644 index 0000000..566b827 --- /dev/null +++ b/services/registration/edit_profile.vis @@ -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 diff --git a/services/registration/edit_profile_swa b/services/registration/edit_profile_swa new file mode 100644 index 0000000..8a12b7d --- /dev/null +++ b/services/registration/edit_profile_swa @@ -0,0 +1 @@ +Wasifu wangu \ No newline at end of file diff --git a/services/registration/edit_yob_menu b/services/registration/edit_yob_menu new file mode 100644 index 0000000..3451781 --- /dev/null +++ b/services/registration/edit_yob_menu @@ -0,0 +1 @@ +Edit year of birth \ No newline at end of file diff --git a/services/registration/edit_yob_menu_swa b/services/registration/edit_yob_menu_swa new file mode 100644 index 0000000..9bb272a --- /dev/null +++ b/services/registration/edit_yob_menu_swa @@ -0,0 +1 @@ +Weka mwaka wa kuzaliwa \ No newline at end of file diff --git a/services/registration/enter_familyname b/services/registration/enter_familyname new file mode 100644 index 0000000..15ffb07 --- /dev/null +++ b/services/registration/enter_familyname @@ -0,0 +1 @@ +Enter family name: diff --git a/services/registration/enter_familyname.vis b/services/registration/enter_familyname.vis new file mode 100644 index 0000000..93def9b --- /dev/null +++ b/services/registration/enter_familyname.vis @@ -0,0 +1,5 @@ +LOAD save_firstname 0 +MOUT back 0 +HALT +INCMP _ 0 +INCMP select_gender * diff --git a/services/registration/enter_familyname_swa b/services/registration/enter_familyname_swa new file mode 100644 index 0000000..e69de29 diff --git a/services/registration/enter_location b/services/registration/enter_location new file mode 100644 index 0000000..da0e148 --- /dev/null +++ b/services/registration/enter_location @@ -0,0 +1 @@ +Enter your location: diff --git a/services/registration/enter_location.vis b/services/registration/enter_location.vis new file mode 100644 index 0000000..f6ccf77 --- /dev/null +++ b/services/registration/enter_location.vis @@ -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 * + + + diff --git a/services/registration/enter_location_swa b/services/registration/enter_location_swa new file mode 100644 index 0000000..41682a2 --- /dev/null +++ b/services/registration/enter_location_swa @@ -0,0 +1 @@ +Weka eneo: \ No newline at end of file diff --git a/services/registration/enter_name b/services/registration/enter_name new file mode 100644 index 0000000..c6851cf --- /dev/null +++ b/services/registration/enter_name @@ -0,0 +1 @@ +Enter your first names: \ No newline at end of file diff --git a/services/registration/enter_name.vis b/services/registration/enter_name.vis new file mode 100644 index 0000000..4126f07 --- /dev/null +++ b/services/registration/enter_name.vis @@ -0,0 +1,4 @@ +MOUT back 0 +HALT +INCMP _ 0 +INCMP enter_familyname * diff --git a/services/registration/enter_name_swa b/services/registration/enter_name_swa new file mode 100644 index 0000000..db04d35 --- /dev/null +++ b/services/registration/enter_name_swa @@ -0,0 +1 @@ +Weka majina yako ya kwanza: diff --git a/services/registration/enter_offerings b/services/registration/enter_offerings new file mode 100644 index 0000000..a9333ba --- /dev/null +++ b/services/registration/enter_offerings @@ -0,0 +1 @@ +Enter the services or goods you offer: \ No newline at end of file diff --git a/services/registration/enter_offerings.vis b/services/registration/enter_offerings.vis new file mode 100644 index 0000000..f48f1ea --- /dev/null +++ b/services/registration/enter_offerings.vis @@ -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 * diff --git a/services/registration/enter_offerings_swa b/services/registration/enter_offerings_swa new file mode 100644 index 0000000..f37e125 --- /dev/null +++ b/services/registration/enter_offerings_swa @@ -0,0 +1 @@ +Weka unachouza \ No newline at end of file diff --git a/services/registration/enter_pin b/services/registration/enter_pin new file mode 100644 index 0000000..cbb44ca --- /dev/null +++ b/services/registration/enter_pin @@ -0,0 +1 @@ +Please enter your PIN: \ No newline at end of file diff --git a/services/registration/enter_pin.vis b/services/registration/enter_pin.vis new file mode 100644 index 0000000..1217074 --- /dev/null +++ b/services/registration/enter_pin.vis @@ -0,0 +1,4 @@ +MOUT back 0 +HALT +INCMP _ 0 +INCMP display_profile_info * diff --git a/services/registration/enter_pin_swa b/services/registration/enter_pin_swa new file mode 100644 index 0000000..bb30cfd --- /dev/null +++ b/services/registration/enter_pin_swa @@ -0,0 +1 @@ +Weka PIN yako \ No newline at end of file diff --git a/services/registration/enter_yob b/services/registration/enter_yob new file mode 100644 index 0000000..54e039e --- /dev/null +++ b/services/registration/enter_yob @@ -0,0 +1 @@ +Enter your year of birth \ No newline at end of file diff --git a/services/registration/enter_yob.vis b/services/registration/enter_yob.vis new file mode 100644 index 0000000..3b3f0d2 --- /dev/null +++ b/services/registration/enter_yob.vis @@ -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 * diff --git a/services/registration/enter_yob_swa b/services/registration/enter_yob_swa new file mode 100644 index 0000000..9bb272a --- /dev/null +++ b/services/registration/enter_yob_swa @@ -0,0 +1 @@ +Weka mwaka wa kuzaliwa \ No newline at end of file diff --git a/services/registration/exit_menu b/services/registration/exit_menu new file mode 100644 index 0000000..1105a55 --- /dev/null +++ b/services/registration/exit_menu @@ -0,0 +1 @@ +Exit \ No newline at end of file diff --git a/services/registration/exit_menu_swa b/services/registration/exit_menu_swa new file mode 100644 index 0000000..474f1ff --- /dev/null +++ b/services/registration/exit_menu_swa @@ -0,0 +1 @@ +Ondoka \ No newline at end of file diff --git a/services/registration/female_menu b/services/registration/female_menu new file mode 100644 index 0000000..b26600f --- /dev/null +++ b/services/registration/female_menu @@ -0,0 +1 @@ +Female diff --git a/services/registration/female_menu_swa b/services/registration/female_menu_swa new file mode 100644 index 0000000..0506300 --- /dev/null +++ b/services/registration/female_menu_swa @@ -0,0 +1 @@ +Mwanamke \ No newline at end of file diff --git a/services/registration/guard_pin_menu b/services/registration/guard_pin_menu new file mode 100644 index 0000000..63ff8dd --- /dev/null +++ b/services/registration/guard_pin_menu @@ -0,0 +1 @@ +Guard my PIN diff --git a/services/registration/guard_pin_menu_swa b/services/registration/guard_pin_menu_swa new file mode 100644 index 0000000..e6f30d4 --- /dev/null +++ b/services/registration/guard_pin_menu_swa @@ -0,0 +1 @@ +Linda PIN yangu diff --git a/services/registration/help_menu b/services/registration/help_menu new file mode 100644 index 0000000..0c64ced --- /dev/null +++ b/services/registration/help_menu @@ -0,0 +1 @@ +Help \ No newline at end of file diff --git a/services/registration/help_menu_swa b/services/registration/help_menu_swa new file mode 100644 index 0000000..393e0c8 --- /dev/null +++ b/services/registration/help_menu_swa @@ -0,0 +1 @@ +Usaidizi \ No newline at end of file diff --git a/services/registration/incorrect_date_format b/services/registration/incorrect_date_format new file mode 100644 index 0000000..308b74c --- /dev/null +++ b/services/registration/incorrect_date_format @@ -0,0 +1,2 @@ +The year of birth you entered is invalid. +Please try again. diff --git a/services/registration/incorrect_date_format.vis b/services/registration/incorrect_date_format.vis new file mode 100644 index 0000000..e94db5d --- /dev/null +++ b/services/registration/incorrect_date_format.vis @@ -0,0 +1,6 @@ +LOAD reset_incorrect_date_format 8 +MOUT retry 1 +MOUT quit 9 +HALT +INCMP enter_yob 1 +INCMP quit 9 diff --git a/services/registration/incorrect_date_format_swa b/services/registration/incorrect_date_format_swa new file mode 100644 index 0000000..bd85f21 --- /dev/null +++ b/services/registration/incorrect_date_format_swa @@ -0,0 +1,2 @@ +Mwaka wa kuzaliwa ulioweka sio sahihi. +Tafadhali jaribu tena. \ No newline at end of file diff --git a/services/registration/incorrect_pin b/services/registration/incorrect_pin new file mode 100644 index 0000000..2bb04e6 --- /dev/null +++ b/services/registration/incorrect_pin @@ -0,0 +1 @@ +Incorrect pin \ No newline at end of file diff --git a/services/registration/incorrect_pin.vis b/services/registration/incorrect_pin.vis new file mode 100644 index 0000000..844f3d6 --- /dev/null +++ b/services/registration/incorrect_pin.vis @@ -0,0 +1,7 @@ +LOAD reset_incorrect 0 +RELOAD reset_incorrect +MOUT retry 1 +MOUT quit 9 +HALT +INCMP _ 1 +INCMP quit 9 diff --git a/services/registration/incorrect_pin_swa b/services/registration/incorrect_pin_swa new file mode 100644 index 0000000..34a0b28 --- /dev/null +++ b/services/registration/incorrect_pin_swa @@ -0,0 +1 @@ +PIN ulioeka sio sahihi \ No newline at end of file diff --git a/services/registration/invalid_amount b/services/registration/invalid_amount new file mode 100644 index 0000000..c4bbe3f --- /dev/null +++ b/services/registration/invalid_amount @@ -0,0 +1 @@ +Amount {{.validate_amount}} is invalid, please try again: \ No newline at end of file diff --git a/services/registration/invalid_amount.vis b/services/registration/invalid_amount.vis new file mode 100644 index 0000000..d5b5f03 --- /dev/null +++ b/services/registration/invalid_amount.vis @@ -0,0 +1,7 @@ +MAP validate_amount +RELOAD reset_transaction_amount +MOUT retry 1 +MOUT quit 9 +HALT +INCMP _ 1 +INCMP quit 9 diff --git a/services/registration/invalid_amount_swa b/services/registration/invalid_amount_swa new file mode 100644 index 0000000..836d7b2 --- /dev/null +++ b/services/registration/invalid_amount_swa @@ -0,0 +1 @@ +Kiwango {{.validate_amount}} sio sahihi, tafadhali weka tena: \ No newline at end of file diff --git a/services/registration/invalid_recipient b/services/registration/invalid_recipient new file mode 100644 index 0000000..0be78bd --- /dev/null +++ b/services/registration/invalid_recipient @@ -0,0 +1 @@ +{{.validate_recipient}} is not registered or invalid, please try again: \ No newline at end of file diff --git a/services/registration/invalid_recipient.vis b/services/registration/invalid_recipient.vis new file mode 100644 index 0000000..09efdde --- /dev/null +++ b/services/registration/invalid_recipient.vis @@ -0,0 +1,7 @@ +MAP validate_recipient +RELOAD transaction_reset +MOUT retry 1 +MOUT quit 9 +HALT +INCMP _ 1 +INCMP quit 9 diff --git a/services/registration/invalid_recipient_swa b/services/registration/invalid_recipient_swa new file mode 100644 index 0000000..39e7804 --- /dev/null +++ b/services/registration/invalid_recipient_swa @@ -0,0 +1 @@ +{{.validate_recipient}} haijasajiliwa au sio sahihi, tafadhali weka tena: \ No newline at end of file diff --git a/services/registration/list_offering b/services/registration/list_offering new file mode 100644 index 0000000..e69de29 diff --git a/services/registration/list_offering.vis b/services/registration/list_offering.vis new file mode 100644 index 0000000..e69de29 diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po new file mode 100644 index 0000000..0a63d07 --- /dev/null +++ b/services/registration/locale/swa/default.po @@ -0,0 +1,8 @@ +msgid "Your account balance is %s" +msgstr "Salio lako ni %s" + +msgid "Your request has been sent. %s will receive %s from %s." +msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s." + +msgid "Thank you for using Sarafu. Goodbye!" +msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!" diff --git a/services/registration/main b/services/registration/main new file mode 100644 index 0000000..bf15ea5 --- /dev/null +++ b/services/registration/main @@ -0,0 +1 @@ +Balance: {{.check_balance}} diff --git a/services/registration/main.vis b/services/registration/main.vis new file mode 100644 index 0000000..ede8296 --- /dev/null +++ b/services/registration/main.vis @@ -0,0 +1,15 @@ +LOAD check_balance 64 +RELOAD check_balance +MAP check_balance +MOUT send 1 +MOUT vouchers 2 +MOUT account 3 +MOUT help 4 +MOUT quit 9 +HALT +INCMP send 1 +INCMP quit 2 +INCMP my_account 3 +INCMP quit 4 +INCMP quit 9 +INCMP . * diff --git a/services/registration/main_swa b/services/registration/main_swa new file mode 100644 index 0000000..b72abf0 --- /dev/null +++ b/services/registration/main_swa @@ -0,0 +1 @@ +Salio: {{.check_balance}} diff --git a/services/registration/male_menu b/services/registration/male_menu new file mode 100644 index 0000000..183883f --- /dev/null +++ b/services/registration/male_menu @@ -0,0 +1 @@ +Male \ No newline at end of file diff --git a/services/registration/male_menu_swa b/services/registration/male_menu_swa new file mode 100644 index 0000000..7afdee9 --- /dev/null +++ b/services/registration/male_menu_swa @@ -0,0 +1 @@ +Mwanaume \ No newline at end of file diff --git a/services/registration/my_account b/services/registration/my_account new file mode 100644 index 0000000..7aa9fe9 --- /dev/null +++ b/services/registration/my_account @@ -0,0 +1 @@ +My Account \ No newline at end of file diff --git a/services/registration/my_account.vis b/services/registration/my_account.vis new file mode 100644 index 0000000..345b1a0 --- /dev/null +++ b/services/registration/my_account.vis @@ -0,0 +1,14 @@ +LOAD reset_allow_update 0 +MOUT profile 1 +MOUT change_language 2 +MOUT check_balance 3 +MOUT check_statement 4 +MOUT pin_options 5 +MOUT my_address 6 +MOUT back 0 +HALT +INCMP _ 0 +INCMP edit_profile 1 +INCMP balances 3 +INCMP pin_management 5 +INCMP address 6 diff --git a/services/registration/my_account_swa b/services/registration/my_account_swa new file mode 100644 index 0000000..c77102f --- /dev/null +++ b/services/registration/my_account_swa @@ -0,0 +1 @@ +Akaunti yangu \ No newline at end of file diff --git a/services/registration/my_address_menu b/services/registration/my_address_menu new file mode 100644 index 0000000..5c13a7d --- /dev/null +++ b/services/registration/my_address_menu @@ -0,0 +1 @@ +My Address \ No newline at end of file diff --git a/services/registration/my_address_menu_swa b/services/registration/my_address_menu_swa new file mode 100644 index 0000000..8ec744e --- /dev/null +++ b/services/registration/my_address_menu_swa @@ -0,0 +1 @@ +Anwani yangu diff --git a/services/registration/my_balance b/services/registration/my_balance new file mode 100644 index 0000000..15c5901 --- /dev/null +++ b/services/registration/my_balance @@ -0,0 +1 @@ +Your balance is: 0.00 SRF \ No newline at end of file diff --git a/services/registration/my_balance.vis b/services/registration/my_balance.vis new file mode 100644 index 0000000..4894944 --- /dev/null +++ b/services/registration/my_balance.vis @@ -0,0 +1,5 @@ +LOAD reset_incorrect 0 +CATCH incorrect_pin 15 1 +CATCH pin_entry 12 0 +LOAD quit_with_balance 0 +HALT diff --git a/services/registration/my_balance_menu b/services/registration/my_balance_menu new file mode 100644 index 0000000..fdd930b --- /dev/null +++ b/services/registration/my_balance_menu @@ -0,0 +1 @@ +My balance \ No newline at end of file diff --git a/services/registration/my_balance_menu_swa b/services/registration/my_balance_menu_swa new file mode 100644 index 0000000..810c386 --- /dev/null +++ b/services/registration/my_balance_menu_swa @@ -0,0 +1 @@ +Salio langu \ No newline at end of file diff --git a/services/registration/my_balance_swa b/services/registration/my_balance_swa new file mode 100644 index 0000000..3f6741f --- /dev/null +++ b/services/registration/my_balance_swa @@ -0,0 +1 @@ +Salio lako ni: 0.00 SRF diff --git a/services/registration/no_menu b/services/registration/no_menu new file mode 100644 index 0000000..7ecb56e --- /dev/null +++ b/services/registration/no_menu @@ -0,0 +1 @@ +no diff --git a/services/registration/no_menu_swa b/services/registration/no_menu_swa new file mode 100644 index 0000000..2ece186 --- /dev/null +++ b/services/registration/no_menu_swa @@ -0,0 +1 @@ +la diff --git a/services/registration/pin_entry b/services/registration/pin_entry new file mode 100644 index 0000000..cbb44ca --- /dev/null +++ b/services/registration/pin_entry @@ -0,0 +1 @@ +Please enter your PIN: \ No newline at end of file diff --git a/services/registration/pin_entry.vis b/services/registration/pin_entry.vis new file mode 100644 index 0000000..2eaf40f --- /dev/null +++ b/services/registration/pin_entry.vis @@ -0,0 +1,4 @@ +LOAD authorize_account 0 +HALT +RELOAD authorize_account +MOVE _ diff --git a/services/registration/pin_entry_swa b/services/registration/pin_entry_swa new file mode 100644 index 0000000..bffbf0f --- /dev/null +++ b/services/registration/pin_entry_swa @@ -0,0 +1 @@ +Tafadhali weka PIN yako diff --git a/services/registration/pin_management b/services/registration/pin_management new file mode 100644 index 0000000..876d1f7 --- /dev/null +++ b/services/registration/pin_management @@ -0,0 +1 @@ +PIN Management diff --git a/services/registration/pin_management.vis b/services/registration/pin_management.vis new file mode 100644 index 0000000..ecd5a8c --- /dev/null +++ b/services/registration/pin_management.vis @@ -0,0 +1,6 @@ +MOUT change_pin 1 +MOUT reset_pin 2 +MOUT guard_pin 3 +MOUT back 0 +HALT +INCMP _ 0 diff --git a/services/registration/pin_management_swa b/services/registration/pin_management_swa new file mode 100644 index 0000000..e69de29 diff --git a/services/registration/pin_options_menu b/services/registration/pin_options_menu new file mode 100644 index 0000000..778d28d --- /dev/null +++ b/services/registration/pin_options_menu @@ -0,0 +1 @@ +PIN options \ No newline at end of file diff --git a/services/registration/pin_options_menu_swa b/services/registration/pin_options_menu_swa new file mode 100644 index 0000000..e47ca0f --- /dev/null +++ b/services/registration/pin_options_menu_swa @@ -0,0 +1 @@ +Mipangilio ya PIN \ No newline at end of file diff --git a/services/registration/profile_menu b/services/registration/profile_menu new file mode 100644 index 0000000..99455ed --- /dev/null +++ b/services/registration/profile_menu @@ -0,0 +1 @@ +Profile diff --git a/services/registration/profile_menu_swa b/services/registration/profile_menu_swa new file mode 100644 index 0000000..b34a86b --- /dev/null +++ b/services/registration/profile_menu_swa @@ -0,0 +1 @@ +Wasifu wangu diff --git a/services/registration/quit.vis b/services/registration/quit.vis new file mode 100644 index 0000000..0c8bb46 --- /dev/null +++ b/services/registration/quit.vis @@ -0,0 +1,2 @@ +LOAD quit 0 +HALT diff --git a/services/registration/reset_pin_menu b/services/registration/reset_pin_menu new file mode 100644 index 0000000..1f5d676 --- /dev/null +++ b/services/registration/reset_pin_menu @@ -0,0 +1 @@ +Reset other's PIN \ No newline at end of file diff --git a/services/registration/reset_pin_menu_swa b/services/registration/reset_pin_menu_swa new file mode 100644 index 0000000..d17e316 --- /dev/null +++ b/services/registration/reset_pin_menu_swa @@ -0,0 +1 @@ +Badili PIN ya mwenzio diff --git a/services/registration/root b/services/registration/root new file mode 100644 index 0000000..3928a82 --- /dev/null +++ b/services/registration/root @@ -0,0 +1 @@ +Welcome to Sarafu Network \ No newline at end of file diff --git a/services/registration/root.vis b/services/registration/root.vis new file mode 100644 index 0000000..14b77f8 --- /dev/null +++ b/services/registration/root.vis @@ -0,0 +1,7 @@ +CATCH select_language 8 0 +CATCH terms 9 0 +LOAD check_account_status 0 +CATCH account_pending 10 1 +CATCH create_pin 18 0 +CATCH main 11 1 +HALT diff --git a/services/registration/root_swa b/services/registration/root_swa new file mode 100644 index 0000000..75bb624 --- /dev/null +++ b/services/registration/root_swa @@ -0,0 +1 @@ +Karibu Sarafu Network \ No newline at end of file diff --git a/services/registration/select_gender b/services/registration/select_gender new file mode 100644 index 0000000..f8a6f47 --- /dev/null +++ b/services/registration/select_gender @@ -0,0 +1 @@ +Select gender: \ No newline at end of file diff --git a/services/registration/select_gender.vis b/services/registration/select_gender.vis new file mode 100644 index 0000000..3029406 --- /dev/null +++ b/services/registration/select_gender.vis @@ -0,0 +1,13 @@ +LOAD save_familyname 0 +CATCH update_success 16 1 +MOUT male 1 +MOUT female 2 +MOUT unspecified 3 +MOUT back 0 +HALT +LOAD save_gender 0 +CATCH pin_entry 23 1 +INCMP _ 0 +INCMP enter_yob 1 +INCMP enter_yob 2 +INCMP enter_yob 3 diff --git a/services/registration/select_gender_swa b/services/registration/select_gender_swa new file mode 100644 index 0000000..2b3a748 --- /dev/null +++ b/services/registration/select_gender_swa @@ -0,0 +1 @@ +Chagua jinsia \ No newline at end of file diff --git a/services/registration/select_language b/services/registration/select_language new file mode 100644 index 0000000..b3d4304 --- /dev/null +++ b/services/registration/select_language @@ -0,0 +1,2 @@ +Welcome to Sarafu Network +Please select a language \ No newline at end of file diff --git a/services/registration/select_language.vis b/services/registration/select_language.vis new file mode 100644 index 0000000..1dd92ae --- /dev/null +++ b/services/registration/select_language.vis @@ -0,0 +1,6 @@ +MOUT english 0 +MOUT kiswahili 1 +HALT +INCMP terms 0 +INCMP terms 1 +INCMP . * diff --git a/services/registration/send b/services/registration/send new file mode 100644 index 0000000..2d5ad69 --- /dev/null +++ b/services/registration/send @@ -0,0 +1 @@ +Enter recipient's phone number: diff --git a/services/registration/send.vis b/services/registration/send.vis new file mode 100644 index 0000000..d0fe211 --- /dev/null +++ b/services/registration/send.vis @@ -0,0 +1,8 @@ +LOAD transaction_reset 0 +MOUT back 0 +HALT +LOAD validate_recipient 20 +RELOAD validate_recipient +CATCH invalid_recipient 13 1 +INCMP _ 0 +INCMP amount * diff --git a/services/registration/send_menu b/services/registration/send_menu new file mode 100644 index 0000000..5f5a837 --- /dev/null +++ b/services/registration/send_menu @@ -0,0 +1 @@ +Send \ No newline at end of file diff --git a/services/registration/send_menu_swa b/services/registration/send_menu_swa new file mode 100644 index 0000000..605c8e8 --- /dev/null +++ b/services/registration/send_menu_swa @@ -0,0 +1 @@ +Tuma \ No newline at end of file diff --git a/services/registration/send_swa b/services/registration/send_swa new file mode 100644 index 0000000..016760e --- /dev/null +++ b/services/registration/send_swa @@ -0,0 +1 @@ +Weka nambari ya simu: \ No newline at end of file diff --git a/services/registration/terms b/services/registration/terms new file mode 100644 index 0000000..05b8c11 --- /dev/null +++ b/services/registration/terms @@ -0,0 +1 @@ +Do you agree to terms and conditions? \ No newline at end of file diff --git a/services/registration/terms.vis b/services/registration/terms.vis new file mode 100644 index 0000000..dea6797 --- /dev/null +++ b/services/registration/terms.vis @@ -0,0 +1,7 @@ +LOAD select_language 0 +RELOAD select_language +MOUT yes 0 +MOUT no 1 +HALT +INCMP create_pin 0 +INCMP quit * diff --git a/services/registration/terms_swa b/services/registration/terms_swa new file mode 100644 index 0000000..7113cd7 --- /dev/null +++ b/services/registration/terms_swa @@ -0,0 +1 @@ +Kwa kutumia hii huduma umekubali sheria na masharti? \ No newline at end of file diff --git a/services/registration/transaction_initiated.vis b/services/registration/transaction_initiated.vis new file mode 100644 index 0000000..b1042f5 --- /dev/null +++ b/services/registration/transaction_initiated.vis @@ -0,0 +1,9 @@ +LOAD reset_incorrect 0 +LOAD get_amount 10 +MAP get_amount +RELOAD get_recipient +MAP get_recipient +RELOAD get_sender +MAP get_sender +LOAD initiate_transaction 0 +HALT diff --git a/services/registration/transaction_pin b/services/registration/transaction_pin new file mode 100644 index 0000000..39a1206 --- /dev/null +++ b/services/registration/transaction_pin @@ -0,0 +1,2 @@ +{{.get_recipient}} will receive {{.validate_amount}} from {{.get_sender}} +Please enter your PIN to confirm: diff --git a/services/registration/transaction_pin.vis b/services/registration/transaction_pin.vis new file mode 100644 index 0000000..5113b6c --- /dev/null +++ b/services/registration/transaction_pin.vis @@ -0,0 +1,15 @@ +MAP validate_amount +RELOAD get_recipient +MAP get_recipient +RELOAD get_sender +MAP get_sender +MOUT back 0 +MOUT quit 9 +HALT +LOAD authorize_account 1 +RELOAD authorize_account +CATCH incorrect_pin 15 1 +INCMP _ 0 +INCMP quit 9 +MOVE transaction_initiated + diff --git a/services/registration/transaction_pin_swa b/services/registration/transaction_pin_swa new file mode 100644 index 0000000..8529f0e --- /dev/null +++ b/services/registration/transaction_pin_swa @@ -0,0 +1,2 @@ +{{.get_recipient}} atapokea {{.validate_amount}} kutoka kwa {{.get_sender}} +Tafadhali weka PIN yako kudhibitisha: \ No newline at end of file diff --git a/services/registration/unspecified_menu b/services/registration/unspecified_menu new file mode 100644 index 0000000..0e1d0c3 --- /dev/null +++ b/services/registration/unspecified_menu @@ -0,0 +1 @@ +Unspecified diff --git a/services/registration/unspecified_menu_swa b/services/registration/unspecified_menu_swa new file mode 100644 index 0000000..009301f --- /dev/null +++ b/services/registration/unspecified_menu_swa @@ -0,0 +1 @@ +Haijabainishwa \ No newline at end of file diff --git a/services/registration/update_success b/services/registration/update_success new file mode 100644 index 0000000..652942a --- /dev/null +++ b/services/registration/update_success @@ -0,0 +1 @@ +Profile updated successfully diff --git a/services/registration/update_success.vis b/services/registration/update_success.vis new file mode 100644 index 0000000..832ef22 --- /dev/null +++ b/services/registration/update_success.vis @@ -0,0 +1,5 @@ +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/update_success_swa b/services/registration/update_success_swa new file mode 100644 index 0000000..19949ad --- /dev/null +++ b/services/registration/update_success_swa @@ -0,0 +1 @@ +Akaunti imeupdatiwa diff --git a/services/registration/view_menu b/services/registration/view_menu new file mode 100644 index 0000000..03add31 --- /dev/null +++ b/services/registration/view_menu @@ -0,0 +1 @@ +View profile \ No newline at end of file diff --git a/services/registration/view_profile b/services/registration/view_profile new file mode 100644 index 0000000..7708bd5 --- /dev/null +++ b/services/registration/view_profile @@ -0,0 +1,2 @@ +My profile: +{{.get_profile_info}} diff --git a/services/registration/view_profile.vis b/services/registration/view_profile.vis new file mode 100644 index 0000000..8bf2b76 --- /dev/null +++ b/services/registration/view_profile.vis @@ -0,0 +1,8 @@ +LOAD get_profile_info 0 +MAP get_profile_info +LOAD reset_incorrect 0 +CATCH incorrect_pin 15 1 +CATCH pin_entry 12 0 +MOUT back 0 +HALT +INCMP _ 0 diff --git a/services/registration/view_profile_swa b/services/registration/view_profile_swa new file mode 100644 index 0000000..8a12b7d --- /dev/null +++ b/services/registration/view_profile_swa @@ -0,0 +1 @@ +Wasifu wangu \ No newline at end of file diff --git a/services/registration/view_swa b/services/registration/view_swa new file mode 100644 index 0000000..7483b4b --- /dev/null +++ b/services/registration/view_swa @@ -0,0 +1 @@ +Angalia Wasifu diff --git a/services/registration/vouchers_menu b/services/registration/vouchers_menu new file mode 100644 index 0000000..5084c32 --- /dev/null +++ b/services/registration/vouchers_menu @@ -0,0 +1 @@ +My Vouchers \ No newline at end of file diff --git a/services/registration/vouchers_menu_swa b/services/registration/vouchers_menu_swa new file mode 100644 index 0000000..64ba54e --- /dev/null +++ b/services/registration/vouchers_menu_swa @@ -0,0 +1 @@ +Sarafu yangu \ No newline at end of file diff --git a/services/registration/yes_menu b/services/registration/yes_menu new file mode 100644 index 0000000..7cfab5b --- /dev/null +++ b/services/registration/yes_menu @@ -0,0 +1 @@ +yes diff --git a/services/registration/yes_menu_swa b/services/registration/yes_menu_swa new file mode 100644 index 0000000..6908a6c --- /dev/null +++ b/services/registration/yes_menu_swa @@ -0,0 +1 @@ +ndio