forked from urdt/ussd
		
	Merge branch 'master' into minor-bug-fixes
This commit is contained in:
		
						commit
						c8c6b05b8a
					
				| @ -20,6 +20,7 @@ import ( | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/common" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| @ -44,14 +45,14 @@ type atRequestParser struct{} | ||||
| func (arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||
| 	rqv, ok := rq.(*http.Request) | ||||
| 	if !ok { | ||||
| 		log.Println("got an invalid request:", rq) | ||||
| 		log.Printf("got an invalid request:", rq) | ||||
| 		return "", handlers.ErrInvalidRequest | ||||
| 	} | ||||
| 
 | ||||
| 	// Capture body (if any) for logging
 | ||||
| 	body, err := io.ReadAll(rqv.Body) | ||||
| 	if err != nil { | ||||
| 		log.Println("failed to read request body:", err) | ||||
| 		log.Printf("failed to read request body:", err) | ||||
| 		return "", fmt.Errorf("failed to read request body: %v", err) | ||||
| 	} | ||||
| 	// Reset the body for further reading
 | ||||
| @ -61,13 +62,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||
| 	bodyLog := map[string]string{"body": string(body)} | ||||
| 	logBytes, err := json.Marshal(bodyLog) | ||||
| 	if err != nil { | ||||
| 		log.Println("failed to marshal request body:", err) | ||||
| 		log.Printf("failed to marshal request body:", err) | ||||
| 	} else { | ||||
| 		log.Println("Received request:", string(logBytes)) | ||||
| 		log.Printf("Received request:", string(logBytes)) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := rqv.ParseForm(); err != nil { | ||||
| 		log.Println("failed to parse form data: %v", err) | ||||
| 		log.Printf("failed to parse form data: %v", err) | ||||
| 		return "", fmt.Errorf("failed to parse form data: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| @ -76,7 +77,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||
| 		return "", fmt.Errorf("no phone number found") | ||||
| 	} | ||||
| 
 | ||||
| 	return phoneNumber, nil | ||||
| 	formattedNumber, err := common.FormatPhoneNumber(phoneNumber) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error: %v\n", err) | ||||
| 		return "", fmt.Errorf("failed to format number") | ||||
| 	} | ||||
| 
 | ||||
| 	return formattedNumber, nil | ||||
| } | ||||
| 
 | ||||
| func (arp *atRequestParser) GetInput(rq any) ([]byte, error) { | ||||
|  | ||||
							
								
								
									
										73
									
								
								common/recipient.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								common/recipient.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // Define the regex patterns as constants
 | ||||
| const ( | ||||
| 	phoneRegex   = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$` | ||||
| 	addressRegex = `^0x[a-fA-F0-9]{40}$` | ||||
| 	aliasRegex   = `^[a-zA-Z0-9]+$` | ||||
| ) | ||||
| 
 | ||||
| // IsValidPhoneNumber checks if the given number is a valid phone number
 | ||||
| func IsValidPhoneNumber(phonenumber string) bool { | ||||
| 	match, _ := regexp.MatchString(phoneRegex, phonenumber) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| // IsValidAddress checks if the given address is a valid Ethereum address
 | ||||
| func IsValidAddress(address string) bool { | ||||
| 	match, _ := regexp.MatchString(addressRegex, address) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| // IsValidAlias checks if the alias is a valid alias format
 | ||||
| func IsValidAlias(alias string) bool { | ||||
| 	match, _ := regexp.MatchString(aliasRegex, alias) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| // CheckRecipient validates the recipient format based on the criteria
 | ||||
| func CheckRecipient(recipient string) (string, error) { | ||||
| 	if IsValidPhoneNumber(recipient) { | ||||
| 		return "phone number", nil | ||||
| 	} | ||||
| 
 | ||||
| 	if IsValidAddress(recipient) { | ||||
| 		return "address", nil | ||||
| 	} | ||||
| 
 | ||||
| 	if IsValidAlias(recipient) { | ||||
| 		return "alias", nil | ||||
| 	} | ||||
| 
 | ||||
| 	return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias") | ||||
| } | ||||
| 
 | ||||
| // FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx".
 | ||||
| func FormatPhoneNumber(phone string) (string, error) { | ||||
| 	if !IsValidPhoneNumber(phone) { | ||||
| 		return "", errors.New("invalid phone number") | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove any leading "+" and spaces
 | ||||
| 	phone = strings.TrimPrefix(phone, "+") | ||||
| 	phone = strings.ReplaceAll(phone, " ", "") | ||||
| 
 | ||||
| 	// Replace leading "0" with "254" if present
 | ||||
| 	if strings.HasPrefix(phone, "0") { | ||||
| 		phone = "254" + phone[1:] | ||||
| 	} | ||||
| 
 | ||||
| 	// Add "+" if not already present
 | ||||
| 	if !strings.HasPrefix(phone, "254") { | ||||
| 		return "", errors.New("unexpected format") | ||||
| 	} | ||||
| 
 | ||||
| 	return "+" + phone, nil | ||||
| } | ||||
							
								
								
									
										119
									
								
								common/transfer_statements.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								common/transfer_statements.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| ) | ||||
| 
 | ||||
| // TransferMetadata helps organize data fields
 | ||||
| type TransferMetadata struct { | ||||
| 	Senders        string | ||||
| 	Recipients     string | ||||
| 	TransferValues string | ||||
| 	Addresses      string | ||||
| 	TxHashes       string | ||||
| 	Dates          string | ||||
| 	Symbols        string | ||||
| 	Decimals       string | ||||
| } | ||||
| 
 | ||||
| // ProcessTransfers converts transfers into formatted strings
 | ||||
| func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata { | ||||
| 	var data TransferMetadata | ||||
| 	var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string | ||||
| 
 | ||||
| 	for _, t := range transfers { | ||||
| 		senders = append(senders, t.Sender) | ||||
| 		recipients = append(recipients, t.Recipient) | ||||
| 
 | ||||
| 		// Scale down the amount
 | ||||
| 		scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals) | ||||
| 		transferValues = append(transferValues, scaledBalance) | ||||
| 
 | ||||
| 		addresses = append(addresses, t.ContractAddress) | ||||
| 		txHashes = append(txHashes, t.TxHash) | ||||
| 		dates = append(dates, fmt.Sprintf("%s", t.DateBlock)) | ||||
| 		symbols = append(symbols, t.TokenSymbol) | ||||
| 		decimals = append(decimals, t.TokenDecimals) | ||||
| 	} | ||||
| 
 | ||||
| 	data.Senders = strings.Join(senders, "\n") | ||||
| 	data.Recipients = strings.Join(recipients, "\n") | ||||
| 	data.TransferValues = strings.Join(transferValues, "\n") | ||||
| 	data.Addresses = strings.Join(addresses, "\n") | ||||
| 	data.TxHashes = strings.Join(txHashes, "\n") | ||||
| 	data.Dates = strings.Join(dates, "\n") | ||||
| 	data.Symbols = strings.Join(symbols, "\n") | ||||
| 	data.Decimals = strings.Join(decimals, "\n") | ||||
| 
 | ||||
| 	return data | ||||
| } | ||||
| 
 | ||||
| // GetTransferData retrieves and matches transfer data
 | ||||
| // returns a formatted string of the full transaction/statement
 | ||||
| func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) { | ||||
| 	keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"} | ||||
| 	data := make(map[string]string) | ||||
| 
 | ||||
| 	for _, key := range keys { | ||||
| 		value, err := db.Get(ctx, []byte(key)) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("failed to get %s: %v", key, err) | ||||
| 		} | ||||
| 		data[key] = string(value) | ||||
| 	} | ||||
| 
 | ||||
| 	// Split the data
 | ||||
| 	senders := strings.Split(string(data["txfrom"]), "\n") | ||||
| 	recipients := strings.Split(string(data["txto"]), "\n") | ||||
| 	values := strings.Split(string(data["txval"]), "\n") | ||||
| 	addresses := strings.Split(string(data["txaddr"]), "\n") | ||||
| 	hashes := strings.Split(string(data["txhash"]), "\n") | ||||
| 	dates := strings.Split(string(data["txdate"]), "\n") | ||||
| 	syms := strings.Split(string(data["txsym"]), "\n") | ||||
| 
 | ||||
| 	// Check if index is within range
 | ||||
| 	if index < 1 || index > len(senders) { | ||||
| 		return "", fmt.Errorf("transaction not found: index %d out of range", index) | ||||
| 	} | ||||
| 
 | ||||
| 	// Adjust for 0-based indexing
 | ||||
| 	i := index - 1 | ||||
| 	transactionType := "received" | ||||
| 	party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i])) | ||||
| 	if strings.TrimSpace(senders[i]) == publicKey { | ||||
| 		transactionType = "sent" | ||||
| 		party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i])) | ||||
| 	} | ||||
| 
 | ||||
| 	formattedDate := formatDate(strings.TrimSpace(dates[i])) | ||||
| 
 | ||||
| 	// Build the full transaction detail
 | ||||
| 	detail := fmt.Sprintf( | ||||
| 		"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s", | ||||
| 		transactionType, | ||||
| 		strings.TrimSpace(values[i]), | ||||
| 		strings.TrimSpace(syms[i]), | ||||
| 		party, | ||||
| 		strings.TrimSpace(addresses[i]), | ||||
| 		strings.TrimSpace(hashes[i]), | ||||
| 		formattedDate, | ||||
| 	) | ||||
| 
 | ||||
| 	return detail, nil | ||||
| } | ||||
| 
 | ||||
| // Helper function to format date in desired output
 | ||||
| func formatDate(dateStr string) string { | ||||
| 	parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("Error parsing date:", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	return parsedDate.Format("2006-01-02 03:04:05 PM") | ||||
| } | ||||
| @ -15,6 +15,7 @@ const ( | ||||
| 	voucherHoldingsPathPrefix  = "/api/v1/holdings" | ||||
| 	voucherTransfersPathPrefix = "/api/v1/transfers/last10" | ||||
| 	voucherDataPathPrefix      = "/api/v1/token" | ||||
| 	AliasPrefix                = "api/v1/alias" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -32,6 +33,7 @@ var ( | ||||
| 	VoucherHoldingsURL  string | ||||
| 	VoucherTransfersURL string | ||||
| 	VoucherDataURL      string | ||||
| 	CheckAliasURL       string | ||||
| ) | ||||
| 
 | ||||
| func setBase() error { | ||||
| @ -66,6 +68,7 @@ func LoadConfig() error { | ||||
| 	VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) | ||||
| 	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) | ||||
| 	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) | ||||
| 	CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										36
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,29 +2,16 @@ module git.grassecon.net/urdt/ussd | ||||
| 
 | ||||
| go 1.23.0 | ||||
| 
 | ||||
| toolchain go1.23.2 | ||||
| 
 | ||||
| require ( | ||||
| 	git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b | ||||
| 	git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a | ||||
| 	github.com/alecthomas/assert/v2 v2.2.2 | ||||
| 	github.com/gofrs/uuid v4.4.0+incompatible | ||||
| 	github.com/grassrootseconomics/eth-custodial v1.3.0-beta | ||||
| 	github.com/peteole/testdata-loader v0.3.0 | ||||
| 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | ||||
| ) | ||||
| 
 | ||||
| require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||
| 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||||
| 	github.com/jackc/pgx/v5 v5.7.1 // indirect | ||||
| 	github.com/jackc/puddle/v2 v2.2.2 // indirect | ||||
| 	github.com/grassrootseconomics/ussd-data-service v1.2.0-beta | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	github.com/kr/text v0.2.0 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect | ||||
| 	golang.org/x/crypto v0.27.0 // indirect | ||||
| 	golang.org/x/sync v0.8.0 // indirect | ||||
| 	golang.org/x/text v0.18.0 // indirect | ||||
| 	github.com/peteole/testdata-loader v0.3.0 | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| @ -33,13 +20,20 @@ require ( | ||||
| 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect | ||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | ||||
| 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect | ||||
| 	github.com/gofrs/uuid v4.4.0+incompatible | ||||
| 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | ||||
| 	github.com/hexops/gotextdiff v1.0.3 // indirect | ||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||
| 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||||
| 	github.com/jackc/pgx/v5 v5.7.1 // indirect | ||||
| 	github.com/jackc/puddle/v2 v2.2.2 // indirect | ||||
| 	github.com/kr/text v0.2.0 // indirect | ||||
| 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect | ||||
| 	github.com/stretchr/objx v0.5.2 // indirect | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	github.com/x448/float16 v0.8.4 // indirect | ||||
| 	golang.org/x/crypto v0.27.0 // indirect | ||||
| 	golang.org/x/sync v0.8.0 // indirect | ||||
| 	golang.org/x/text v0.18.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | ||||
| github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= | ||||
| github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= | ||||
| github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | ||||
| @ -18,8 +18,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 | ||||
| github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= | ||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= | ||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= | ||||
| github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU= | ||||
| github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw= | ||||
| github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= | ||||
| github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= | ||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= | ||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | ||||
| github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | ||||
|  | ||||
| @ -118,6 +118,9 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn | ||||
| 	ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions) | ||||
| 	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) | ||||
| 	ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) | ||||
| 
 | ||||
| 	return ussdHandlers, nil | ||||
| } | ||||
|  | ||||
| @ -35,12 +35,17 @@ var ( | ||||
| 	errResponse    *api.ErrResponse | ||||
| ) | ||||
| 
 | ||||
| // Define the regex patterns as  constants
 | ||||
| // Define the regex patterns as constants
 | ||||
| const ( | ||||
| 	phoneRegex = `(\(\d{3}\)\s?|\d{3}[-.\s]?)?\d{3}[-.\s]?\d{4}` | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| // FlagManager handles centralized flag management
 | ||||
| type FlagManager struct { | ||||
| 	parser *asm.FlagParser | ||||
| @ -95,17 +100,6 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util | ||||
| 	return h, nil | ||||
| } | ||||
| 
 | ||||
| // isValidPIN checks whether the given input is a 4 digit number
 | ||||
| func isValidPIN(pin string) bool { | ||||
| 	match, _ := regexp.MatchString(pinPattern, pin) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| func isValidPhoneNumber(phonenumber string) bool { | ||||
| 	match, _ := regexp.MatchString(phoneRegex, phonenumber) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { | ||||
| 	if h.pe != nil { | ||||
| 		panic("persister already set") | ||||
| @ -878,7 +872,7 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input | ||||
| 	} | ||||
| 	blockedNumber := string(input) | ||||
| 	_, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY) | ||||
| 	if !isValidPhoneNumber(blockedNumber) { | ||||
| 	if !common.IsValidPhoneNumber(blockedNumber) { | ||||
| 		res.FlagSet = append(res.FlagSet, flag_unregistered_number) | ||||
| 		return res, nil | ||||
| 	} | ||||
| @ -899,10 +893,9 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input | ||||
| 	return res, nil | ||||
| } | ||||
| 
 | ||||
| // ValidateRecipient validates that the given input is a valid phone number.
 | ||||
| // ValidateRecipient validates that the given input is valid.
 | ||||
| func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { | ||||
| 	var res resource.Result | ||||
| 	var err error | ||||
| 	store := h.userdataStore | ||||
| 
 | ||||
| 	sessionId, ok := ctx.Value("SessionId").(string) | ||||
| @ -910,13 +903,16 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by | ||||
| 		return res, fmt.Errorf("missing session") | ||||
| 	} | ||||
| 
 | ||||
| 	recipient := string(input) | ||||
| 
 | ||||
| 	flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") | ||||
| 	flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") | ||||
| 	flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") | ||||
| 
 | ||||
| 	recipient := string(input) | ||||
| 
 | ||||
| 	if recipient != "0" { | ||||
| 		if !isValidPhoneNumber(recipient) { | ||||
| 		recipientType, err := common.CheckRecipient(recipient) | ||||
| 		if err != nil { | ||||
| 			// Invalid recipient format (not a phone number, address, or valid alias format)
 | ||||
| 			res.FlagSet = append(res.FlagSet, flag_invalid_recipient) | ||||
| 			res.Content = recipient | ||||
| 
 | ||||
| @ -930,25 +926,61 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by | ||||
| 			return res, err | ||||
| 		} | ||||
| 
 | ||||
| 		publicKey, err := store.ReadEntry(ctx, recipient, common.DATA_PUBLIC_KEY) | ||||
| 		if err != nil { | ||||
| 			if db.IsNotFound(err) { | ||||
| 				logg.InfoCtxf(ctx, "Unregistered number") | ||||
| 
 | ||||
| 				res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) | ||||
| 				res.Content = recipient | ||||
| 
 | ||||
| 				return res, nil | ||||
| 		switch recipientType { | ||||
| 		case "phone number": | ||||
| 			// format the phone number
 | ||||
| 			formattedNumber, err := common.FormatPhoneNumber(recipient) | ||||
| 			if err != nil { | ||||
| 				logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err) | ||||
| 				return res, err | ||||
| 			} | ||||
| 
 | ||||
| 			logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) | ||||
| 			return res, err | ||||
| 		} | ||||
| 			// Check if the phone number is registered
 | ||||
| 			publicKey, err := store.ReadEntry(ctx, formattedNumber, common.DATA_PUBLIC_KEY) | ||||
| 			if err != nil { | ||||
| 				if db.IsNotFound(err) { | ||||
| 					logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient) | ||||
| 					res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) | ||||
| 					res.Content = recipient | ||||
| 					return res, nil | ||||
| 				} | ||||
| 
 | ||||
| 		err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey) | ||||
| 		if err != nil { | ||||
| 			logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err) | ||||
| 			return res, nil | ||||
| 				logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) | ||||
| 				return res, err | ||||
| 			} | ||||
| 
 | ||||
| 			// Save the publicKey as the recipient
 | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey) | ||||
| 			if err != nil { | ||||
| 				logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err) | ||||
| 				return res, err | ||||
| 			} | ||||
| 
 | ||||
| 		case "address": | ||||
| 			// Save the valid Ethereum address as the recipient
 | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recipient)) | ||||
| 			if err != nil { | ||||
| 				logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", recipient, "error", err) | ||||
| 				return res, err | ||||
| 			} | ||||
| 
 | ||||
| 		case "alias": | ||||
| 			// Call the API to validate and retrieve the address for the alias
 | ||||
| 			r, aliasErr := h.accountService.CheckAliasAddress(ctx, recipient) | ||||
| 			if aliasErr != nil { | ||||
| 				res.FlagSet = append(res.FlagSet, flag_api_error) | ||||
| 				res.Content = recipient | ||||
| 
 | ||||
| 				logg.ErrorCtxf(ctx, "failed on CheckAliasAddress", "error", aliasErr) | ||||
| 				return res, err | ||||
| 			} | ||||
| 
 | ||||
| 			// Alias validation succeeded, save the Ethereum address
 | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(r.Address)) | ||||
| 			if err != nil { | ||||
| 				logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", r.Address, "error", err) | ||||
| 				return res, err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| @ -1638,3 +1670,172 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by | ||||
| 
 | ||||
| 	return res, nil | ||||
| } | ||||
| 
 | ||||
| // CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb
 | ||||
| func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []byte) (resource.Result, error) { | ||||
| 	var res resource.Result | ||||
| 	sessionId, ok := ctx.Value("SessionId").(string) | ||||
| 	if !ok { | ||||
| 		return res, fmt.Errorf("missing session") | ||||
| 	} | ||||
| 
 | ||||
| 	flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers") | ||||
| 	flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") | ||||
| 
 | ||||
| 	store := h.userdataStore | ||||
| 	publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Fetch transactions from the API using the public key
 | ||||
| 	transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey)) | ||||
| 	if err != nil { | ||||
| 		res.FlagSet = append(res.FlagSet, flag_api_error) | ||||
| 		logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Return if there are no transactions
 | ||||
| 	if len(transactionsResp) == 0 { | ||||
| 		res.FlagSet = append(res.FlagSet, flag_no_transfers) | ||||
| 		return res, nil | ||||
| 	} | ||||
| 
 | ||||
| 	data := common.ProcessTransfers(transactionsResp) | ||||
| 
 | ||||
| 	// Store all transaction data
 | ||||
| 	dataMap := map[string]string{ | ||||
| 		"txfrom": data.Senders, | ||||
| 		"txto":   data.Recipients, | ||||
| 		"txval":  data.TransferValues, | ||||
| 		"txaddr": data.Addresses, | ||||
| 		"txhash": data.TxHashes, | ||||
| 		"txdate": data.Dates, | ||||
| 		"txsym":  data.Symbols, | ||||
| 		"txdeci": data.Decimals, | ||||
| 	} | ||||
| 
 | ||||
| 	for key, value := range dataMap { | ||||
| 		if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != nil { | ||||
| 			logg.ErrorCtxf(ctx, "failed to write to prefixDb", "error", err) | ||||
| 			return res, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	res.FlagReset = append(res.FlagReset, flag_no_transfers) | ||||
| 
 | ||||
| 	return res, nil | ||||
| } | ||||
| 
 | ||||
| // GetTransactionsList fetches the list of transactions and formats them
 | ||||
| func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) { | ||||
| 	var res resource.Result | ||||
| 	sessionId, ok := ctx.Value("SessionId").(string) | ||||
| 	if !ok { | ||||
| 		return res, fmt.Errorf("missing session") | ||||
| 	} | ||||
| 	store := h.userdataStore | ||||
| 	publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Read transactions from the store and format them
 | ||||
| 	TransactionSenders, err := h.prefixDb.Get(ctx, []byte("txfrom")) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "Failed to read the TransactionSenders from prefixDb", "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 	TransactionSyms, err := h.prefixDb.Get(ctx, []byte("txsym")) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "Failed to read the TransactionSyms from prefixDb", "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 	TransactionValues, err := h.prefixDb.Get(ctx, []byte("txval")) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "Failed to read the TransactionValues from prefixDb", "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 	TransactionDates, err := h.prefixDb.Get(ctx, []byte("txdate")) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "Failed to read the TransactionDates from prefixDb", "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Parse the data
 | ||||
| 	senders := strings.Split(string(TransactionSenders), "\n") | ||||
| 	syms := strings.Split(string(TransactionSyms), "\n") | ||||
| 	values := strings.Split(string(TransactionValues), "\n") | ||||
| 	dates := strings.Split(string(TransactionDates), "\n") | ||||
| 
 | ||||
| 	var formattedTransactions []string | ||||
| 	for i := 0; i < len(senders); i++ { | ||||
| 		sender := strings.TrimSpace(senders[i]) | ||||
| 		sym := strings.TrimSpace(syms[i]) | ||||
| 		value := strings.TrimSpace(values[i]) | ||||
| 		date := strings.Split(strings.TrimSpace(dates[i]), " ")[0] | ||||
| 
 | ||||
| 		status := "received" | ||||
| 		if sender == string(publicKey) { | ||||
| 			status = "sent" | ||||
| 		} | ||||
| 
 | ||||
| 		formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date)) | ||||
| 	} | ||||
| 
 | ||||
| 	res.Content = strings.Join(formattedTransactions, "\n") | ||||
| 
 | ||||
| 	return res, nil | ||||
| } | ||||
| 
 | ||||
| // ViewTransactionStatement retrieves the transaction statement
 | ||||
| // and displays it to the user
 | ||||
| func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, input []byte) (resource.Result, error) { | ||||
| 	var res resource.Result | ||||
| 	sessionId, ok := ctx.Value("SessionId").(string) | ||||
| 	if !ok { | ||||
| 		return res, fmt.Errorf("missing session") | ||||
| 	} | ||||
| 	store := h.userdataStore | ||||
| 	publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) | ||||
| 		return res, err | ||||
| 	} | ||||
| 
 | ||||
| 	flag_incorrect_statement, _ := h.flagManager.GetFlag("flag_incorrect_statement") | ||||
| 
 | ||||
| 	inputStr := string(input) | ||||
| 	if inputStr == "0" || inputStr == "99" || inputStr == "11" || inputStr == "22" { | ||||
| 		res.FlagReset = append(res.FlagReset, flag_incorrect_statement) | ||||
| 		return res, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// Convert input string to integer
 | ||||
| 	index, err := strconv.Atoi(strings.TrimSpace(inputStr)) | ||||
| 	if err != nil { | ||||
| 		return res, fmt.Errorf("invalid input: must be a number between 1 and 10") | ||||
| 	} | ||||
| 
 | ||||
| 	if index < 1 || index > 10 { | ||||
| 		return res, fmt.Errorf("invalid input: index must be between 1 and 10") | ||||
| 	} | ||||
| 
 | ||||
| 	statement, err := common.GetTransferData(ctx, h.prefixDb, string(publicKey), index) | ||||
| 	if err != nil { | ||||
| 		return res, fmt.Errorf("failed to retrieve transfer data: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if statement == "" { | ||||
| 		res.FlagSet = append(res.FlagSet, flag_incorrect_statement) | ||||
| 		return res, nil | ||||
| 	} | ||||
| 
 | ||||
| 	res.FlagReset = append(res.FlagReset, flag_incorrect_statement) | ||||
| 	res.Content = statement | ||||
| 
 | ||||
| 	return res, nil | ||||
| } | ||||
|  | ||||
| @ -4,8 +4,8 @@ import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -114,3 +114,8 @@ func(tdb *ThreadGdbmDb) Close() error { | ||||
| 	tdb.db = nil | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) { | ||||
| 	logg.Warnf("method not implemented for thread gdbm db") | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| @ -47,3 +47,8 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(*models.TokenTransferResponse), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1) | ||||
| } | ||||
|  | ||||
| @ -33,8 +33,8 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	return []dataserviceapi.TokenHoldings { | ||||
| 		dataserviceapi.TokenHoldings { | ||||
| 	return []dataserviceapi.TokenHoldings{ | ||||
| 		dataserviceapi.TokenHoldings{ | ||||
| 			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", | ||||
| 			TokenSymbol:     "SRF", | ||||
| 			TokenDecimals:   "6", | ||||
| @ -56,3 +56,7 @@ func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, | ||||
| 		TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	return &dataserviceapi.AliasAddress{}, nil | ||||
| } | ||||
|  | ||||
| @ -2,8 +2,8 @@ | ||||
| 
 | ||||
| package testtag | ||||
| 
 | ||||
| import "git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| import "git.grassecon.net/urdt/ussd/remote" | ||||
| 
 | ||||
| var ( | ||||
| 	AccountService server.AccountServiceInterface | ||||
| 	AccountService remote.AccountServiceInterface | ||||
| ) | ||||
|  | ||||
| @ -61,7 +61,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "Enter recipient's phone number:\n0:Back" | ||||
|                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "000", | ||||
| @ -69,7 +69,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "Enter recipient's phone number:\n0:Back" | ||||
|                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "0712345678", | ||||
|  | ||||
| @ -24,6 +24,7 @@ type AccountServiceInterface interface { | ||||
| 	FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) | ||||
| 	VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) | ||||
| 	TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) | ||||
| 	CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) | ||||
| } | ||||
| 
 | ||||
| type AccountService struct { | ||||
| @ -209,6 +210,26 @@ func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, t | ||||
| 	return &r, nil | ||||
| } | ||||
| 
 | ||||
| // CheckAliasAddress retrieves the address of an alias from the API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - alias: The alias of the user.
 | ||||
| func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	var r dataserviceapi.AliasAddress | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.CheckAliasURL, alias) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	return &r, err | ||||
| } | ||||
| 
 | ||||
| func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||
| 	var okResponse api.OKResponse | ||||
| 	var errResponse api.ErrResponse | ||||
|  | ||||
							
								
								
									
										1
									
								
								services/registration/check_statement
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/check_statement
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Please enter your PIN to view statement: | ||||
							
								
								
									
										12
									
								
								services/registration/check_statement.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								services/registration/check_statement.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| LOAD check_transactions 0 | ||||
| RELOAD check_transactions | ||||
| CATCH no_transfers flag_no_transfers 1 | ||||
| LOAD authorize_account 6 | ||||
| MOUT back 0 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| RELOAD authorize_account | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| INCMP _ 0 | ||||
| INCMP quit 9 | ||||
| INCMP transactions * | ||||
							
								
								
									
										1
									
								
								services/registration/check_statement_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/check_statement_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Tafadhali weka PIN yako kuona taarifa ya matumizi: | ||||
| @ -11,5 +11,6 @@ INCMP main 0 | ||||
| INCMP edit_profile 1 | ||||
| INCMP change_language 2 | ||||
| INCMP balances 3 | ||||
| INCMP check_statement 4 | ||||
| INCMP pin_management 5 | ||||
| INCMP address 6 | ||||
|  | ||||
							
								
								
									
										1
									
								
								services/registration/no_transfers
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/no_transfers
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| No transfers history | ||||
							
								
								
									
										5
									
								
								services/registration/no_transfers.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								services/registration/no_transfers.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| MOUT back 0 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP ^ 0 | ||||
| INCMP quit 9 | ||||
							
								
								
									
										1
									
								
								services/registration/no_transfers_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/no_transfers_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Hakuna historia kwa akaunti yako | ||||
| @ -19,3 +19,5 @@ flag,flag_api_call_error,25,this is set when communication to an external servic | ||||
| flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher | ||||
| flag,flag_admin_privilege,27,this is set when a user has admin privileges. | ||||
| flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action | ||||
| flag,flag_no_transfers,29,this is set when a user does not have any transactions | ||||
| flag,flag_incorrect_statement,30,this is set when the selected statement is invalid | ||||
|  | ||||
| 
 | 
| @ -1 +1 @@ | ||||
| Enter recipient's phone number: | ||||
| Enter recipient's phone number/address/alias: | ||||
							
								
								
									
										1
									
								
								services/registration/transactions
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/transactions
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {{.get_transactions}} | ||||
							
								
								
									
										15
									
								
								services/registration/transactions.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								services/registration/transactions.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| LOAD get_transactions 0 | ||||
| MAP get_transactions | ||||
| MOUT back 0 | ||||
| MOUT quit 99 | ||||
| MNEXT next 11 | ||||
| MPREV prev 22 | ||||
| HALT | ||||
| LOAD view_statement 0 | ||||
| RELOAD view_statement | ||||
| CATCH . flag_incorrect_statement 1 | ||||
| INCMP ^ 0 | ||||
| INCMP quit 99 | ||||
| INCMP > 11 | ||||
| INCMP < 22 | ||||
| INCMP view_statement * | ||||
							
								
								
									
										1
									
								
								services/registration/transactions_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/transactions_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {{.get_transactions}} | ||||
							
								
								
									
										1
									
								
								services/registration/view_statement
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/view_statement
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {{.view_statement}} | ||||
							
								
								
									
										10
									
								
								services/registration/view_statement.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								services/registration/view_statement.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| MAP view_statement | ||||
| MOUT back 0 | ||||
| MOUT quit 9 | ||||
| MNEXT next 11 | ||||
| MPREV prev 22 | ||||
| HALT | ||||
| INCMP _ 0 | ||||
| INCMP quit 9 | ||||
| INCMP > 11 | ||||
| INCMP < 22 | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user