forked from urdt/ussd
Compare commits
97 Commits
lash/store
...
master
Author | SHA1 | Date | |
---|---|---|---|
47b5ff0435 | |||
|
25867cf05e | ||
d5a2680500 | |||
|
d950b10b50 | ||
|
bcb3ab905e | ||
|
3ed9caf16d | ||
|
86464c31d2 | ||
5ee10d8e14 | |||
62f3681b9e | |||
3ce1435591 | |||
f65c458daa | |||
|
67007fcd48 | ||
|
f1b258fa6d | ||
d2fce05461 | |||
68ac237449 | |||
162e6c1934 | |||
8bd025f2b2 | |||
9d6e25e184 | |||
c26f5683f6 | |||
91dc9ce82f | |||
0fe48a30fa | |||
58edfa01a2 | |||
3830c12a57 | |||
f1fd690a7b | |||
491b7424a9 | |||
29ce4b83bd | |||
ca8df5989a | |||
82b4365d16 | |||
98db85511b | |||
99a4d3ff42 | |||
d95c7abea4 | |||
fd1ac85a1b | |||
c899c098f6 | |||
5ca6a74274 | |||
48d63fb43f | |||
e666c58644 | |||
e980586910 | |||
ffd5be1f1f | |||
ed1aeecf7d | |||
3b69f3d38d | |||
|
cd58f5ae33 | ||
7a535f796a | |||
7c4c73125e | |||
|
c7dbe1d88f | ||
4ea52bf3fb | |||
be2ea3a2f0 | |||
8217ea8fdc | |||
3c73fc7188 | |||
1311a0cab9 | |||
|
3bcd48e5a7 | ||
|
0e12c0ee4e | ||
3caee98cdb | |||
db7c9bf56d | |||
0a332ec501 | |||
90367fe53e | |||
50c006546c | |||
e8c171a82e | |||
58a60f2c81 | |||
0820e1b9f2 | |||
46edf2b819 | |||
11eb61ba35 | |||
813b92af78 | |||
5579991d66 | |||
f4f4fdd3ac | |||
be215d3f75 | |||
235af3519d | |||
1292851226 | |||
dfd0a0994b | |||
97fcdda12f | |||
055c2db790 | |||
ecfdab47a8 | |||
fda68231ea | |||
d08afff443 | |||
17ba6a06ba | |||
dbd59a4023 | |||
5534706189 | |||
5428626c3f | |||
9b33117cb1 | |||
70b2fa4ac2 | |||
fd6ff86579 | |||
549782f230 | |||
8cf4848b45 | |||
7aea2af9a1 | |||
5cd791aae7 | |||
df5e5f1a4b | |||
64c1fe5276 | |||
f38ea59569 | |||
6cc285d1e8 | |||
0d7f7aaca1 | |||
f8ea2daa73 | |||
e05f8e7291 | |||
2383e8ead3 | |||
1a4ee0d3e1 | |||
6f3b30e2fe | |||
b1e4b63c6a | |||
3129e8210e | |||
5d8de80a18 |
@ -1,109 +1,39 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
"git.defalsify.org/vise.git/engine"
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/common"
|
|
||||||
"git.grassecon.net/urdt/ussd/config"
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
"git.grassecon.net/urdt/ussd/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
"git.grassecon.net/urdt/ussd/internal/http/at"
|
||||||
|
httpserver "git.grassecon.net/urdt/ussd/internal/http/at"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
"git.grassecon.net/urdt/ussd/remote"
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
|
build = "dev"
|
||||||
build = "dev"
|
menuSeparator = ": "
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
initializers.LoadEnvVariables()
|
initializers.LoadEnvVariables()
|
||||||
}
|
}
|
||||||
|
|
||||||
type atRequestParser struct{}
|
|
||||||
|
|
||||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
|
|
||||||
rqv, ok := rq.(*http.Request)
|
|
||||||
if !ok {
|
|
||||||
logg.Warnf("got an invalid request", "req", rq)
|
|
||||||
return "", handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture body (if any) for logging
|
|
||||||
body, err := io.ReadAll(rqv.Body)
|
|
||||||
if err != nil {
|
|
||||||
logg.Warnf("failed to read request body", "err", err)
|
|
||||||
return "", fmt.Errorf("failed to read request body: %v", err)
|
|
||||||
}
|
|
||||||
// Reset the body for further reading
|
|
||||||
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
|
||||||
|
|
||||||
// Log the body as JSON
|
|
||||||
bodyLog := map[string]string{"body": string(body)}
|
|
||||||
logBytes, err := json.Marshal(bodyLog)
|
|
||||||
if err != nil {
|
|
||||||
logg.Warnf("failed to marshal request body", "err", err)
|
|
||||||
} else {
|
|
||||||
logg.Debugf("received request", "bytes", logBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rqv.ParseForm(); err != nil {
|
|
||||||
logg.Warnf("failed to parse form data", "err", err)
|
|
||||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
phoneNumber := rqv.FormValue("phoneNumber")
|
|
||||||
if phoneNumber == "" {
|
|
||||||
return "", fmt.Errorf("no phone number found")
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
|
||||||
if err != nil {
|
|
||||||
logg.Warnf("failed to format phone number", "err", err)
|
|
||||||
return "", fmt.Errorf("failed to format number")
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedNumber, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
|
|
||||||
rqv, ok := rq.(*http.Request)
|
|
||||||
if !ok {
|
|
||||||
return nil, handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
if err := rqv.ParseForm(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
text := rqv.FormValue("text")
|
|
||||||
|
|
||||||
parts := strings.Split(text, "*")
|
|
||||||
if len(parts) == 0 {
|
|
||||||
return nil, fmt.Errorf("no input found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(parts[len(parts)-1]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
@ -130,9 +60,10 @@ func main() {
|
|||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
cfg := engine.Config{
|
cfg := engine.Config{
|
||||||
Root: "root",
|
Root: "root",
|
||||||
OutputSize: uint32(size),
|
OutputSize: uint32(size),
|
||||||
FlagCount: uint32(128),
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
}
|
}
|
||||||
|
|
||||||
if engineDebug {
|
if engineDebug {
|
||||||
@ -190,7 +121,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer stateStore.Close()
|
defer stateStore.Close()
|
||||||
|
|
||||||
rp := &atRequestParser{}
|
rp := &at.ATRequestParser{
|
||||||
|
Context: ctx,
|
||||||
|
}
|
||||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||||
sh := httpserver.NewATSessionHandler(bsh)
|
sh := httpserver.NewATSessionHandler(bsh)
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla()
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
|
menuSeparator = ": "
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -70,9 +71,10 @@ func main() {
|
|||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
cfg := engine.Config{
|
cfg := engine.Config{
|
||||||
Root: "root",
|
Root: "root",
|
||||||
OutputSize: uint32(size),
|
OutputSize: uint32(size),
|
||||||
FlagCount: uint32(128),
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
}
|
}
|
||||||
|
|
||||||
if engineDebug {
|
if engineDebug {
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla()
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
|
menuSeparator = ": "
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -58,9 +59,10 @@ func main() {
|
|||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
cfg := engine.Config{
|
cfg := engine.Config{
|
||||||
Root: "root",
|
Root: "root",
|
||||||
OutputSize: uint32(size),
|
OutputSize: uint32(size),
|
||||||
FlagCount: uint32(128),
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
}
|
}
|
||||||
|
|
||||||
if engineDebug {
|
if engineDebug {
|
||||||
|
14
cmd/main.go
14
cmd/main.go
@ -18,8 +18,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla()
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
|
menuSeparator = ": "
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -49,10 +50,11 @@ func main() {
|
|||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
cfg := engine.Config{
|
cfg := engine.Config{
|
||||||
Root: "root",
|
Root: "root",
|
||||||
SessionId: sessionId,
|
SessionId: sessionId,
|
||||||
OutputSize: uint32(size),
|
OutputSize: uint32(size),
|
||||||
FlagCount: uint32(128),
|
FlagCount: uint32(128),
|
||||||
|
MenuSeparator: menuSeparator,
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceDir := scriptDir
|
resourceDir := scriptDir
|
||||||
|
33
common/pin.go
Normal file
33
common/pin.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define the regex pattern as a constant
|
||||||
|
const (
|
||||||
|
pinPattern = `^\d{4}$`
|
||||||
|
)
|
||||||
|
|
||||||
|
// checks whether the given input is a 4 digit number
|
||||||
|
func IsValidPIN(pin string) bool {
|
||||||
|
match, _ := regexp.MatchString(pinPattern, pin)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
|
||||||
|
func HashPIN(pin string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPIN compareS the hashed PIN with the plaintext PIN
|
||||||
|
func VerifyPIN(hashedPIN, pin string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
|
||||||
|
return err == nil
|
||||||
|
}
|
173
common/pin_test.go
Normal file
173
common/pin_test.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsValidPIN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pin string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN with 4 digits",
|
||||||
|
pin: "1234",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid PIN with leading zeros",
|
||||||
|
pin: "0001",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with less than 4 digits",
|
||||||
|
pin: "123",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with more than 4 digits",
|
||||||
|
pin: "12345",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with letters",
|
||||||
|
pin: "abcd",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with special characters",
|
||||||
|
pin: "12@#",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty PIN",
|
||||||
|
pin: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual := IsValidPIN(tt.pin)
|
||||||
|
if actual != tt.expected {
|
||||||
|
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashPIN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pin string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN with 4 digits",
|
||||||
|
pin: "1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid PIN with leading zeros",
|
||||||
|
pin: "0001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty PIN",
|
||||||
|
pin: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
hashedPIN, err := HashPIN(tt.pin)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hashedPIN == "" {
|
||||||
|
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the hash can be verified with bcrypt
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
|
||||||
|
if tt.pin != "" && err != nil {
|
||||||
|
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyMigratedHashPin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
pin string
|
||||||
|
hash string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pin: "1234",
|
||||||
|
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.pin, func(t *testing.T) {
|
||||||
|
ok := VerifyPIN(tt.hash, tt.pin)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyPIN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pin string
|
||||||
|
hashedPIN string
|
||||||
|
shouldPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN verification",
|
||||||
|
pin: "1234",
|
||||||
|
hashedPIN: hashPINHelper("1234"),
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN verification with incorrect PIN",
|
||||||
|
pin: "5678",
|
||||||
|
hashedPIN: hashPINHelper("1234"),
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN verification with empty PIN",
|
||||||
|
pin: "",
|
||||||
|
hashedPIN: hashPINHelper("1234"),
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN verification with invalid hash",
|
||||||
|
pin: "1234",
|
||||||
|
hashedPIN: "invalidhash",
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := VerifyPIN(tt.hashedPIN, tt.pin)
|
||||||
|
if result != tt.shouldPass {
|
||||||
|
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to hash a PIN for testing purposes
|
||||||
|
func hashPINHelper(pin string) string {
|
||||||
|
hashedPIN, err := HashPIN(pin)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to hash PIN for test setup: " + err.Error())
|
||||||
|
}
|
||||||
|
return hashedPIN
|
||||||
|
}
|
@ -8,14 +8,15 @@ import (
|
|||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StoreToDb(store *UserDataStore) db.Db {
|
func StoreToDb(store *UserDataStore) db.Db {
|
||||||
return store.Db
|
return store.Db
|
||||||
}
|
}
|
||||||
|
|
||||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
|
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
|
||||||
return storage.NewSubPrefixDb(store.Db, pfx)
|
return dbstorage.NewSubPrefixDb(store.Db, pfx)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageServices interface {
|
type StorageServices interface {
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
|
|||||||
|
|
||||||
// GetTransferData retrieves and matches transfer data
|
// GetTransferData retrieves and matches transfer data
|
||||||
// returns a formatted string of the full transaction/statement
|
// returns a formatted string of the full transaction/statement
|
||||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
|
func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
|
||||||
keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
|
keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
|
||||||
data := make(map[DataTyp]string)
|
data := make(map[DataTyp]string)
|
||||||
|
|
||||||
@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string,
|
|||||||
|
|
||||||
// Adjust for 0-based indexing
|
// Adjust for 0-based indexing
|
||||||
i := index - 1
|
i := index - 1
|
||||||
transactionType := "received"
|
transactionType := "Received"
|
||||||
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
|
party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i]))
|
||||||
if strings.TrimSpace(senders[i]) == publicKey {
|
if strings.TrimSpace(senders[i]) == publicKey {
|
||||||
transactionType = "sent"
|
transactionType = "Sent"
|
||||||
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
|
party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedDate := formatDate(strings.TrimSpace(dates[i]))
|
formattedDate := formatDate(strings.TrimSpace(dates[i]))
|
||||||
|
|
||||||
// Build the full transaction detail
|
// Build the full transaction detail
|
||||||
detail := fmt.Sprintf(
|
detail := fmt.Sprintf(
|
||||||
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
|
"%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s",
|
||||||
transactionType,
|
transactionType,
|
||||||
strings.TrimSpace(values[i]),
|
strings.TrimSpace(values[i]),
|
||||||
strings.TrimSpace(syms[i]),
|
strings.TrimSpace(syms[i]),
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ func ScaleDownBalance(balance, decimals string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetVoucherData retrieves and matches voucher data
|
// GetVoucherData retrieves and matches voucher data
|
||||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||||
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
|
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
|
||||||
data := make(map[DataTyp]string)
|
data := make(map[DataTyp]string)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
visedb "git.defalsify.org/vise.git/db"
|
visedb "git.defalsify.org/vise.git/db"
|
||||||
memdb "git.defalsify.org/vise.git/db/mem"
|
memdb "git.defalsify.org/vise.git/db/mem"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ func TestGetVoucherData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
||||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||||
|
|
||||||
// Test voucher data
|
// Test voucher data
|
||||||
mockData := map[DataTyp][]byte{
|
mockData := map[DataTyp][]byte{
|
||||||
|
@ -11,13 +11,9 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
DebugCap |= 1
|
DebugCap |= 1
|
||||||
dbTypStr[db.DATATYPE_STATE] = "internal state"
|
dbTypStr[db.DATATYPE_STATE] = "internal state"
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT] = "account"
|
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_CREATED] = "account created"
|
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
|
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
|
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_CUSTODIAL_ID] = "custodial id"
|
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
|
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_STATUS] = "account status"
|
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
|
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
|
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
|
||||||
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
|
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"git.grassecon.net/urdt/ussd/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
"git.grassecon.net/urdt/ussd/debug"
|
"git.grassecon.net/urdt/ussd/debug"
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,13 +48,14 @@ func main() {
|
|||||||
|
|
||||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
store.SetPrefix(db.DATATYPE_USERDATA)
|
||||||
|
|
||||||
d, err := store.Dump(ctx, []byte(sessionId))
|
d, err := store.Dump(ctx, []byte(sessionId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ func main() {
|
|||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Printf("%vValue: %v\n\n", o, v)
|
fmt.Printf("%vValue: %v\n\n", o, string(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.Close()
|
err = store.Close()
|
||||||
|
4
go.mod
4
go.mod
@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
|
||||||
github.com/alecthomas/assert/v2 v2.2.2
|
github.com/alecthomas/assert/v2 v2.2.2
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||||
@ -11,6 +11,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/peteole/testdata-loader v0.3.0
|
github.com/peteole/testdata-loader v0.3.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
golang.org/x/crypto v0.27.0
|
||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +33,6 @@ require (
|
|||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
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/sync v0.8.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -1,5 +1,5 @@
|
|||||||
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 h1:GYUVXRUtMpA40T4COeAduoay6CIgXjD5cfDYZOTFIKw=
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
|
||||||
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
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/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
||||||
|
@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/asm"
|
"git.defalsify.org/vise.git/asm"
|
||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
@ -64,7 +65,11 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
|
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
|
||||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
|
replaceSeparatorFunc := func(input string) string {
|
||||||
|
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -111,7 +116,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
|
|||||||
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
|
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
|
||||||
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
|
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
|
||||||
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
|
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
|
||||||
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
|
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckBlockedNumPinMisMatch)
|
||||||
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
|
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
|
||||||
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
|
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
|
||||||
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
|
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -24,27 +23,16 @@ import (
|
|||||||
"git.grassecon.net/urdt/ussd/remote"
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
"gopkg.in/leonelquinteros/gotext.v1"
|
"gopkg.in/leonelquinteros/gotext.v1"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
|
logg = logging.NewVanilla().WithDomain("ussdmenuhandler").WithContextKey("session-id")
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
translationDir = path.Join(scriptDir, "locale")
|
translationDir = path.Join(scriptDir, "locale")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define the regex patterns as constants
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlagManager handles centralized flag management
|
// FlagManager handles centralized flag management
|
||||||
type FlagManager struct {
|
type FlagManager struct {
|
||||||
parser *asm.FlagParser
|
parser *asm.FlagParser
|
||||||
@ -69,18 +57,20 @@ func (fm *FlagManager) GetFlag(label string) (uint32, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
pe *persist.Persister
|
pe *persist.Persister
|
||||||
st *state.State
|
st *state.State
|
||||||
ca cache.Memory
|
ca cache.Memory
|
||||||
userdataStore common.DataStore
|
userdataStore common.DataStore
|
||||||
adminstore *utils.AdminStore
|
adminstore *utils.AdminStore
|
||||||
flagManager *asm.FlagParser
|
flagManager *asm.FlagParser
|
||||||
accountService remote.AccountServiceInterface
|
accountService remote.AccountServiceInterface
|
||||||
prefixDb storage.PrefixDb
|
prefixDb dbstorage.PrefixDb
|
||||||
profile *models.Profile
|
profile *models.Profile
|
||||||
|
ReplaceSeparatorFunc func(string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) {
|
// NewHandlers creates a new instance of the Handlers struct with the provided dependencies.
|
||||||
|
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface, replaceSeparatorFunc func(string) string) (*Handlers, error) {
|
||||||
if userdataStore == nil {
|
if userdataStore == nil {
|
||||||
return nil, fmt.Errorf("cannot create handler with nil userdata store")
|
return nil, fmt.Errorf("cannot create handler with nil userdata store")
|
||||||
}
|
}
|
||||||
@ -90,19 +80,21 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
|||||||
|
|
||||||
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
||||||
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
||||||
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
|
prefixDb := dbstorage.NewSubPrefixDb(userdataStore, prefix)
|
||||||
|
|
||||||
h := &Handlers{
|
h := &Handlers{
|
||||||
userdataStore: userDb,
|
userdataStore: userDb,
|
||||||
flagManager: appFlags,
|
flagManager: appFlags,
|
||||||
adminstore: adminstore,
|
adminstore: adminstore,
|
||||||
accountService: accountService,
|
accountService: accountService,
|
||||||
prefixDb: prefixDb,
|
prefixDb: prefixDb,
|
||||||
profile: &models.Profile{Max: 6},
|
profile: &models.Profile{Max: 6},
|
||||||
|
ReplaceSeparatorFunc: replaceSeparatorFunc,
|
||||||
}
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPersister sets persister instance to the handlers.
|
||||||
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
||||||
if h.pe != nil {
|
if h.pe != nil {
|
||||||
panic("persister already set")
|
panic("persister already set")
|
||||||
@ -111,6 +103,7 @@ func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init initializes the handler for a new session.
|
||||||
func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var r resource.Result
|
var r resource.Result
|
||||||
if h.pe == nil {
|
if h.pe == nil {
|
||||||
@ -124,9 +117,17 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
|
|||||||
h.st = h.pe.GetState()
|
h.st = h.pe.GetState()
|
||||||
h.ca = h.pe.GetMemory()
|
h.ca = h.pe.GetMemory()
|
||||||
|
|
||||||
sessionId, _ := ctx.Value("SessionId").(string)
|
if len(input) == 0 {
|
||||||
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
// move to the top node
|
||||||
|
h.st.Code = []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
|
if ok {
|
||||||
|
context.WithValue(ctx, "session-id", sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
||||||
isAdmin, _ := h.adminstore.IsAdmin(sessionId)
|
isAdmin, _ := h.adminstore.IsAdmin(sessionId)
|
||||||
|
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
@ -149,7 +150,7 @@ func (h *Handlers) Exit() {
|
|||||||
h.pe = nil
|
h.pe = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLanguage sets the language across the menu
|
// SetLanguage sets the language across the menu.
|
||||||
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -173,6 +174,7 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handles the account creation when no existing account is present for the session and stores associated data in the user data store.
|
||||||
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
|
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
|
||||||
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
|
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
|
||||||
r, err := h.accountService.CreateAccount(ctx)
|
r, err := h.accountService.CreateAccount(ctx)
|
||||||
@ -205,9 +207,9 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAccount checks if any account exists on the JSON data file, and if not
|
// CreateAccount checks if any account exists on the JSON data file, and if not,
|
||||||
// creates an account on the API,
|
// creates an account on the API,
|
||||||
// sets the default values and flags
|
// sets the default values and flags.
|
||||||
func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -231,19 +233,22 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
// CheckBlockedNumPinMisMatch checks if the provided PIN matches a temporary PIN stored for a blocked number.
|
||||||
|
func (h *Handlers) CheckBlockedNumPinMisMatch(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
res := resource.Result{}
|
res := resource.Result{}
|
||||||
flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
|
flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
// Get blocked number from storage.
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
|
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
|
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
// Get temporary PIN for the blocked number.
|
||||||
temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE)
|
temporaryPin, err := store.ReadEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
||||||
@ -257,6 +262,7 @@ func (h *Handlers) CheckPinMisMatch(ctx context.Context, sym string, input []byt
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyNewPin checks if a new PIN meets the required format criteria.
|
||||||
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
res := resource.Result{}
|
res := resource.Result{}
|
||||||
_, ok := ctx.Value("SessionId").(string)
|
_, ok := ctx.Value("SessionId").(string)
|
||||||
@ -265,8 +271,8 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
|
|||||||
}
|
}
|
||||||
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
|
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
|
||||||
pinInput := string(input)
|
pinInput := string(input)
|
||||||
// Validate that the PIN is a 4-digit number
|
// Validate that the PIN is a 4-digit number.
|
||||||
if isValidPIN(pinInput) {
|
if common.IsValidPIN(pinInput) {
|
||||||
res.FlagSet = append(res.FlagSet, flag_valid_pin)
|
res.FlagSet = append(res.FlagSet, flag_valid_pin)
|
||||||
} else {
|
} else {
|
||||||
res.FlagReset = append(res.FlagReset, flag_valid_pin)
|
res.FlagReset = append(res.FlagReset, flag_valid_pin)
|
||||||
@ -275,9 +281,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_VALUE
|
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_VALUE,
|
||||||
// during the account creation process
|
// during the account creation process
|
||||||
// and during the change PIN process
|
// and during the change PIN process.
|
||||||
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -290,8 +296,8 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
|
|||||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
||||||
accountPIN := string(input)
|
accountPIN := string(input)
|
||||||
|
|
||||||
// Validate that the PIN is a 4-digit number
|
// Validate that the PIN is a 4-digit number.
|
||||||
if !isValidPIN(accountPIN) {
|
if !common.IsValidPIN(accountPIN) {
|
||||||
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
@ -306,6 +312,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveOthersTemporaryPin allows authorized users to set temporary PINs for blocked numbers.
|
||||||
func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -316,12 +323,14 @@ func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input
|
|||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
temporaryPin := string(input)
|
temporaryPin := string(input)
|
||||||
|
// First, we retrieve the blocked number associated with this session
|
||||||
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
|
blockedNumber, err := store.ReadEntry(ctx, sessionId, common.DATA_BLOCKED_NUMBER)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
|
logg.ErrorCtxf(ctx, "failed to read blockedNumber entry with", "key", common.DATA_BLOCKED_NUMBER, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then we save the temporary PIN for that blocked number
|
||||||
err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin))
|
err = store.WriteEntry(ctx, string(blockedNumber), common.DATA_TEMPORARY_VALUE, []byte(temporaryPin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryPin, "error", err)
|
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "value", temporaryPin, "error", err)
|
||||||
@ -331,6 +340,7 @@ func (h *Handlers) SaveOthersTemporaryPin(ctx context.Context, sym string, input
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfirmPinChange validates user's new PIN. If input matches the temporary PIN, saves it as the new account PIN.
|
||||||
func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
@ -349,10 +359,20 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
|
|||||||
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
|
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
|
||||||
} else {
|
} else {
|
||||||
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
|
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
|
||||||
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err)
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the hashed PIN as the new account PIN
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "hashedPIN value", hashedPIN, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
@ -360,7 +380,7 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
|
|||||||
|
|
||||||
// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
|
// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
|
||||||
// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
|
// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
|
||||||
// to access the main menu
|
// to access the main menu.
|
||||||
func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -384,18 +404,26 @@ func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte
|
|||||||
res.FlagSet = append(res.FlagSet, flag_pin_set)
|
res.FlagSet = append(res.FlagSet, flag_pin_set)
|
||||||
} else {
|
} else {
|
||||||
res.FlagSet = []uint32{flag_pin_mismatch}
|
res.FlagSet = []uint32{flag_pin_mismatch}
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err)
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "value", hashedPIN, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// codeFromCtx retrieves language codes from the context that can be used for handling translations
|
// retrieves language codes from the context that can be used for handling translations.
|
||||||
func codeFromCtx(ctx context.Context) string {
|
func codeFromCtx(ctx context.Context) string {
|
||||||
var code string
|
var code string
|
||||||
if ctx.Value("Language") != nil {
|
if ctx.Value("Language") != nil {
|
||||||
@ -702,7 +730,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
if len(input) == 4 {
|
if len(input) == 4 {
|
||||||
if bytes.Equal(input, AccountPin) {
|
if common.VerifyPIN(string(AccountPin), string(input)) {
|
||||||
if h.st.MatchFlag(flag_account_authorized, false) {
|
if h.st.MatchFlag(flag_account_authorized, false) {
|
||||||
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
||||||
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
|
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
|
||||||
@ -729,7 +757,7 @@ func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []by
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setback sets the flag_back_set flag when the navigation is back
|
// Setback sets the flag_back_set flag when the navigation is back.
|
||||||
func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
//TODO:
|
//TODO:
|
||||||
@ -742,7 +770,7 @@ func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resou
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckAccountStatus queries the API using the TrackingId and sets flags
|
// CheckAccountStatus queries the API using the TrackingId and sets flags
|
||||||
// based on the account status
|
// based on the account status.
|
||||||
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -782,7 +810,7 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit displays the Thank you message and exits the menu
|
// Quit displays the Thank you message and exits the menu.
|
||||||
func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -797,7 +825,7 @@ func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuitWithHelp displays helpline information then exits the menu
|
// QuitWithHelp displays helpline information then exits the menu.
|
||||||
func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -812,7 +840,7 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyYob verifies the length of the given input
|
// VerifyYob verifies the length of the given input.
|
||||||
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -834,7 +862,7 @@ func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (res
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetIncorrectYob resets the incorrect date format flag after a new attempt
|
// ResetIncorrectYob resets the incorrect date format flag after a new attempt.
|
||||||
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -844,7 +872,7 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckBalance retrieves the balance of the active voucher and sets
|
// CheckBalance retrieves the balance of the active voucher and sets
|
||||||
// the balance as the result content
|
// the balance as the result content.
|
||||||
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -894,9 +922,12 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchCommunityBalance retrieves and displays the balance for community accounts in user's preferred language.
|
||||||
func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
// retrieve the language code from the context
|
||||||
code := codeFromCtx(ctx)
|
code := codeFromCtx(ctx)
|
||||||
|
// Initialize the localization system with the appropriate translation directory
|
||||||
l := gotext.NewLocale(translationDir, code)
|
l := gotext.NewLocale(translationDir, code)
|
||||||
l.AddDomain("default")
|
l.AddDomain("default")
|
||||||
//TODO:
|
//TODO:
|
||||||
@ -905,6 +936,10 @@ func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetOthersPin handles the PIN reset process for other users' accounts by:
|
||||||
|
// 1. Retrieving the blocked phone number from the session
|
||||||
|
// 2. Fetching the temporary PIN associated with that number
|
||||||
|
// 3. Updating the account PIN with the temporary PIN
|
||||||
func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
@ -922,7 +957,15 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
|
|||||||
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
|
||||||
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
@ -930,6 +973,8 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetUnregisteredNumber clears the unregistered number flag in the system,
|
||||||
|
// indicating that a number's registration status should no longer be marked as unregistered.
|
||||||
func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number")
|
flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number")
|
||||||
@ -937,6 +982,8 @@ func (h *Handlers) ResetUnregisteredNumber(ctx context.Context, sym string, inpu
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateBlockedNumber performs validation of phone numbers, specifically for blocked numbers in the system.
|
||||||
|
// It checks phone number format and verifies registration status.
|
||||||
func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -1065,7 +1112,7 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TransactionReset resets the previous transaction data (Recipient and Amount)
|
// TransactionReset resets the previous transaction data (Recipient and Amount)
|
||||||
// as well as the invalid flags
|
// as well as the invalid flags.
|
||||||
func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -1118,7 +1165,7 @@ func (h *Handlers) InviteValidRecipient(ctx context.Context, sym string, input [
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetTransactionAmount resets the transaction amount and invalid flag
|
// ResetTransactionAmount resets the transaction amount and invalid flag.
|
||||||
func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -1248,7 +1295,7 @@ func (h *Handlers) RetrieveBlockedNumber(ctx context.Context, sym string, input
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSender returns the sessionId (phoneNumber)
|
// GetSender returns the sessionId (phoneNumber).
|
||||||
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -1262,7 +1309,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAmount retrieves the amount from teh Gdbm Db
|
// GetAmount retrieves the amount from teh Gdbm Db.
|
||||||
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -1286,7 +1333,7 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result
|
// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result.
|
||||||
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -1336,9 +1383,12 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCurrentProfileInfo retrieves specific profile fields based on the current state of the USSD session.
|
||||||
|
// Uses flag management system to track profile field status and handle menu navigation.
|
||||||
func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var profileInfo []byte
|
var profileInfo []byte
|
||||||
|
var defaultValue string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set")
|
flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set")
|
||||||
@ -1355,6 +1405,17 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
language, ok := ctx.Value("Language").(lang.Language)
|
||||||
|
if !ok {
|
||||||
|
return res, fmt.Errorf("value for 'Language' is not of type lang.Language")
|
||||||
|
}
|
||||||
|
code := language.Code
|
||||||
|
if code == "swa" {
|
||||||
|
defaultValue = "Haipo"
|
||||||
|
} else {
|
||||||
|
defaultValue = "Not Provided"
|
||||||
|
}
|
||||||
|
|
||||||
sm, _ := h.st.Where()
|
sm, _ := h.st.Where()
|
||||||
parts := strings.SplitN(sm, "_", 2)
|
parts := strings.SplitN(sm, "_", 2)
|
||||||
filename := parts[1]
|
filename := parts[1]
|
||||||
@ -1371,7 +1432,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
|
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsNotFound(err) {
|
if db.IsNotFound(err) {
|
||||||
res.Content = "Not provided"
|
res.Content = defaultValue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err)
|
logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", common.DATA_FIRST_NAME, err)
|
||||||
@ -1383,7 +1444,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
|
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsNotFound(err) {
|
if db.IsNotFound(err) {
|
||||||
res.Content = "Not provided"
|
res.Content = defaultValue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err)
|
logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", common.DATA_FAMILY_NAME, err)
|
||||||
@ -1396,7 +1457,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
|
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsNotFound(err) {
|
if db.IsNotFound(err) {
|
||||||
res.Content = "Not provided"
|
res.Content = defaultValue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err)
|
logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", common.DATA_GENDER, err)
|
||||||
@ -1408,7 +1469,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB)
|
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_YOB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsNotFound(err) {
|
if db.IsNotFound(err) {
|
||||||
res.Content = "Not provided"
|
res.Content = defaultValue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err)
|
logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", common.DATA_YOB, err)
|
||||||
@ -1420,7 +1481,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
|
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsNotFound(err) {
|
if db.IsNotFound(err) {
|
||||||
res.Content = "Not provided"
|
res.Content = defaultValue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err)
|
logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", common.DATA_LOCATION, err)
|
||||||
@ -1432,7 +1493,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
|
profileInfo, err = store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsNotFound(err) {
|
if db.IsNotFound(err) {
|
||||||
res.Content = "Not provided"
|
res.Content = defaultValue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err)
|
logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", common.DATA_OFFERINGS, err)
|
||||||
@ -1447,6 +1508,7 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProfileInfo provides a comprehensive view of a user's profile.
|
||||||
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var defaultValue string
|
var defaultValue string
|
||||||
@ -1515,7 +1577,7 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultVoucher retrieves the current vouchers
|
// SetDefaultVoucher retrieves the current vouchers
|
||||||
// and sets the first as the default voucher, if no active voucher is set
|
// and sets the first as the default voucher, if no active voucher is set.
|
||||||
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
var err error
|
||||||
@ -1600,7 +1662,7 @@ func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []by
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
|
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
|
||||||
// them to gdbm
|
// them to gdbm.
|
||||||
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
@ -1672,7 +1734,7 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVoucherList fetches the list of vouchers and formats them
|
// GetVoucherList fetches the list of vouchers and formats them.
|
||||||
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -1683,13 +1745,15 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Content = string(voucherData)
|
formattedData := h.ReplaceSeparatorFunc(string(voucherData))
|
||||||
|
|
||||||
|
res.Content = string(formattedData)
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewVoucher retrieves the token holding and balance from the subprefixDB
|
// ViewVoucher retrieves the token holding and balance from the subprefixDB
|
||||||
// and displays it to the user for them to select it
|
// and displays it to the user for them to select it.
|
||||||
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
@ -1730,7 +1794,7 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetVoucher retrieves the temp voucher data and sets it as the active data
|
// SetVoucher retrieves the temp voucher data and sets it as the active data.
|
||||||
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
|
|
||||||
@ -1756,7 +1820,7 @@ func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (re
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVoucherDetails retrieves the voucher details
|
// GetVoucherDetails retrieves the voucher details.
|
||||||
func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
@ -1788,7 +1852,7 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb
|
// 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) {
|
func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
@ -1846,13 +1910,14 @@ func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []by
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTransactionsList fetches the list of transactions and formats them
|
// GetTransactionsList fetches the list of transactions and formats them.
|
||||||
func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
|
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1895,12 +1960,14 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
|
|||||||
value := strings.TrimSpace(values[i])
|
value := strings.TrimSpace(values[i])
|
||||||
date := strings.Split(strings.TrimSpace(dates[i]), " ")[0]
|
date := strings.Split(strings.TrimSpace(dates[i]), " ")[0]
|
||||||
|
|
||||||
status := "received"
|
status := "Received"
|
||||||
if sender == string(publicKey) {
|
if sender == string(publicKey) {
|
||||||
status = "sent"
|
status = "Sent"
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date))
|
// Use the ReplaceSeparator function for the menu separator
|
||||||
|
transactionLine := fmt.Sprintf("%d%s%s %s %s %s", i+1, h.ReplaceSeparatorFunc(":"), status, value, sym, date)
|
||||||
|
formattedTransactions = append(formattedTransactions, transactionLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Content = strings.Join(formattedTransactions, "\n")
|
res.Content = strings.Join(formattedTransactions, "\n")
|
||||||
@ -1909,7 +1976,7 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ViewTransactionStatement retrieves the transaction statement
|
// ViewTransactionStatement retrieves the transaction statement
|
||||||
// and displays it to the user
|
// and displays it to the user.
|
||||||
func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
@ -1957,6 +2024,7 @@ func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, inp
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handles bulk updates of profile information.
|
||||||
func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error {
|
func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error {
|
||||||
var err error
|
var err error
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
@ -1979,21 +2047,22 @@ func (h *Handlers) insertProfileItems(ctx context.Context, sessionId string, res
|
|||||||
for index, profileItem := range h.profile.ProfileItems {
|
for index, profileItem := range h.profile.ProfileItems {
|
||||||
// Ensure the profileItem is not "0"(is set)
|
// Ensure the profileItem is not "0"(is set)
|
||||||
if profileItem != "0" {
|
if profileItem != "0" {
|
||||||
err = store.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem))
|
|
||||||
if err != nil {
|
|
||||||
logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the flag for the current index
|
|
||||||
flag, _ := h.flagManager.GetFlag(profileFlagNames[index])
|
flag, _ := h.flagManager.GetFlag(profileFlagNames[index])
|
||||||
res.FlagSet = append(res.FlagSet, flag)
|
isProfileItemSet := h.st.MatchFlag(flag, true)
|
||||||
|
if !isProfileItemSet {
|
||||||
|
err = store.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.FlagSet = append(res.FlagSet, flag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags
|
// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags.
|
||||||
func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
|
@ -5,15 +5,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/cache"
|
||||||
"git.defalsify.org/vise.git/lang"
|
"git.defalsify.org/vise.git/lang"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.defalsify.org/vise.git/state"
|
"git.defalsify.org/vise.git/state"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
|
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
|
||||||
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||||
"git.grassecon.net/urdt/ussd/models"
|
"git.grassecon.net/urdt/ussd/models"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/common"
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
@ -32,6 +35,11 @@ var (
|
|||||||
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
|
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mockReplaceSeparator function
|
||||||
|
var mockReplaceSeparator = func(input string) string {
|
||||||
|
return strings.ReplaceAll(input, ":", ": ")
|
||||||
|
}
|
||||||
|
|
||||||
// InitializeTestStore sets up and returns an in-memory database and store.
|
// InitializeTestStore sets up and returns an in-memory database and store.
|
||||||
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
|
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@ -51,14 +59,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
|
|||||||
return ctx, store
|
return ctx, store
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb {
|
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb {
|
||||||
db := memdb.NewMemDb()
|
db := memdb.NewMemDb()
|
||||||
err := db.Connect(ctx, "")
|
err := db.Connect(ctx, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
||||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||||
|
|
||||||
return spdb
|
return spdb
|
||||||
}
|
}
|
||||||
@ -67,12 +75,15 @@ func TestNewHandlers(t *testing.T) {
|
|||||||
_, store := InitializeTestStore(t)
|
_, store := InitializeTestStore(t)
|
||||||
|
|
||||||
fm, err := NewFlagManager(flagsPath)
|
fm, err := NewFlagManager(flagsPath)
|
||||||
accountService := testservice.TestAccountService{}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf(err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountService := testservice.TestAccountService{}
|
||||||
|
|
||||||
|
// Test case for valid UserDataStore
|
||||||
t.Run("Valid UserDataStore", func(t *testing.T) {
|
t.Run("Valid UserDataStore", func(t *testing.T) {
|
||||||
handlers, err := NewHandlers(fm.parser, store, nil, &accountService)
|
handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
@ -82,23 +93,130 @@ func TestNewHandlers(t *testing.T) {
|
|||||||
if handlers.userdataStore == nil {
|
if handlers.userdataStore == nil {
|
||||||
t.Fatal("expected userdataStore to be set in handlers")
|
t.Fatal("expected userdataStore to be set in handlers")
|
||||||
}
|
}
|
||||||
|
if handlers.ReplaceSeparatorFunc == nil {
|
||||||
|
t.Fatal("expected ReplaceSeparatorFunc to be set in handlers")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ReplaceSeparatorFunc functionality
|
||||||
|
input := "1:Menu item"
|
||||||
|
expectedOutput := "1: Menu item"
|
||||||
|
if handlers.ReplaceSeparatorFunc(input) != expectedOutput {
|
||||||
|
t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test case for nil userdataStore
|
// Test case for nil UserDataStore
|
||||||
t.Run("Nil UserDataStore", func(t *testing.T) {
|
t.Run("Nil UserDataStore", func(t *testing.T) {
|
||||||
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService)
|
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected an error, got none")
|
t.Fatal("expected an error, got none")
|
||||||
}
|
}
|
||||||
if handlers != nil {
|
if handlers != nil {
|
||||||
t.Fatal("expected handlers to be nil")
|
t.Fatal("expected handlers to be nil")
|
||||||
}
|
}
|
||||||
if err.Error() != "cannot create handler with nil userdata store" {
|
expectedError := "cannot create handler with nil userdata store"
|
||||||
t.Fatalf("expected specific error, got %v", err)
|
if err.Error() != expectedError {
|
||||||
|
t.Fatalf("expected error '%s', got '%v'", expectedError, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
sessionId := "session123"
|
||||||
|
ctx, store := InitializeTestStore(t)
|
||||||
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
|
||||||
|
fm, err := NewFlagManager(flagsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
st := state.NewState(128)
|
||||||
|
ca := cache.NewCache()
|
||||||
|
|
||||||
|
flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() (*Handlers, context.Context)
|
||||||
|
input []byte
|
||||||
|
expectedResult resource.Result
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Handler not ready",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
return &Handlers{}, ctx
|
||||||
|
},
|
||||||
|
input: []byte("1"),
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "State and memory initialization",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
|
||||||
|
h := &Handlers{
|
||||||
|
flagManager: fm.parser,
|
||||||
|
adminstore: adminstore,
|
||||||
|
pe: pe,
|
||||||
|
}
|
||||||
|
return h, context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
},
|
||||||
|
input: []byte("1"),
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagReset: []uint32{flag_admin_privilege},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-admin session initialization",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca)
|
||||||
|
h := &Handlers{
|
||||||
|
flagManager: fm.parser,
|
||||||
|
adminstore: adminstore,
|
||||||
|
pe: pe,
|
||||||
|
}
|
||||||
|
return h, context.WithValue(context.Background(), "SessionId", "0712345678")
|
||||||
|
},
|
||||||
|
input: []byte("1"),
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagReset: []uint32{flag_admin_privilege},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Move to top node on empty input",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
|
||||||
|
h := &Handlers{
|
||||||
|
flagManager: fm.parser,
|
||||||
|
adminstore: adminstore,
|
||||||
|
pe: pe,
|
||||||
|
}
|
||||||
|
st.Code = []byte("some pending bytecode")
|
||||||
|
return h, context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
},
|
||||||
|
input: []byte(""),
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagReset: []uint32{flag_admin_privilege},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h, testCtx := tt.setup()
|
||||||
|
res, err := h.Init(testCtx, "", tt.input)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error occurred")
|
||||||
|
assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateAccount(t *testing.T) {
|
func TestCreateAccount(t *testing.T) {
|
||||||
sessionId := "session123"
|
sessionId := "session123"
|
||||||
ctx, store := InitializeTestStore(t)
|
ctx, store := InitializeTestStore(t)
|
||||||
@ -929,7 +1047,14 @@ func TestAuthorize(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN))
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(accountPIN)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -1381,59 +1506,6 @@ func TestQuit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidPIN(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
pin string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid PIN with 4 digits",
|
|
||||||
pin: "1234",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid PIN with leading zeros",
|
|
||||||
pin: "0001",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with less than 4 digits",
|
|
||||||
pin: "123",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with more than 4 digits",
|
|
||||||
pin: "12345",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with letters",
|
|
||||||
pin: "abcd",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with special characters",
|
|
||||||
pin: "12@#",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty PIN",
|
|
||||||
pin: "",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
actual := isValidPIN(tt.pin)
|
|
||||||
if actual != tt.expected {
|
|
||||||
t.Errorf("isValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateAmount(t *testing.T) {
|
func TestValidateAmount(t *testing.T) {
|
||||||
fm, err := NewFlagManager(flagsPath)
|
fm, err := NewFlagManager(flagsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1680,7 +1752,7 @@ func TestGetProfile(t *testing.T) {
|
|||||||
result: resource.Result{
|
result: resource.Result{
|
||||||
Content: fmt.Sprintf(
|
Content: fmt.Sprintf(
|
||||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1692,7 +1764,7 @@ func TestGetProfile(t *testing.T) {
|
|||||||
result: resource.Result{
|
result: resource.Result{
|
||||||
Content: fmt.Sprintf(
|
Content: fmt.Sprintf(
|
||||||
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
|
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
|
||||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1704,7 +1776,7 @@ func TestGetProfile(t *testing.T) {
|
|||||||
result: resource.Result{
|
result: resource.Result{
|
||||||
Content: fmt.Sprintf(
|
Content: fmt.Sprintf(
|
||||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1982,26 +2054,31 @@ func TestCheckVouchers(t *testing.T) {
|
|||||||
|
|
||||||
func TestGetVoucherList(t *testing.T) {
|
func TestGetVoucherList(t *testing.T) {
|
||||||
sessionId := "session123"
|
sessionId := "session123"
|
||||||
|
|
||||||
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
|
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
|
||||||
|
|
||||||
spdb := InitializeTestSubPrefixDb(t, ctx)
|
spdb := InitializeTestSubPrefixDb(t, ctx)
|
||||||
|
|
||||||
|
// Initialize Handlers
|
||||||
h := &Handlers{
|
h := &Handlers{
|
||||||
prefixDb: spdb,
|
prefixDb: spdb,
|
||||||
|
ReplaceSeparatorFunc: mockReplaceSeparator,
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedSym := []byte("1:SRF\n2:MILO")
|
mockSyms := []byte("1:SRF\n2:MILO")
|
||||||
|
|
||||||
// Put voucher sym data from the store
|
// Put voucher sym data from the store
|
||||||
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym)
|
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedSyms := []byte("1: SRF\n2: MILO")
|
||||||
|
|
||||||
res, err := h.GetVoucherList(ctx, "", []byte(""))
|
res, err := h.GetVoucherList(ctx, "", []byte(""))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, res.Content, string(expectedSym))
|
assert.Equal(t, res.Content, string(expectedSyms))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestViewVoucher(t *testing.T) {
|
func TestViewVoucher(t *testing.T) {
|
||||||
|
121
internal/http/at/parse.go
Normal file
121
internal/http/at/parse.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package at
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ATRequestParser struct {
|
||||||
|
Context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arp *ATRequestParser) GetSessionId(rq any) (string, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
logg.Warnf("got an invalid request", "req", rq)
|
||||||
|
return "", handlers.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture body (if any) for logging
|
||||||
|
body, err := io.ReadAll(rqv.Body)
|
||||||
|
if err != nil {
|
||||||
|
logg.Warnf("failed to read request body", "err", err)
|
||||||
|
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||||
|
}
|
||||||
|
// Reset the body for further reading
|
||||||
|
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
|
|
||||||
|
// Log the body as JSON
|
||||||
|
bodyLog := map[string]string{"body": string(body)}
|
||||||
|
logBytes, err := json.Marshal(bodyLog)
|
||||||
|
if err != nil {
|
||||||
|
logg.Warnf("failed to marshal request body", "err", err)
|
||||||
|
} else {
|
||||||
|
decodedStr := string(logBytes)
|
||||||
|
sessionId, err := extractATSessionId(decodedStr)
|
||||||
|
if err != nil {
|
||||||
|
context.WithValue(arp.Context, "at-session-id", sessionId)
|
||||||
|
}
|
||||||
|
logg.Debugf("Received request:", decodedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rqv.ParseForm(); err != nil {
|
||||||
|
logg.Warnf("failed to parse form data", "err", err)
|
||||||
|
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
phoneNumber := rqv.FormValue("phoneNumber")
|
||||||
|
if phoneNumber == "" {
|
||||||
|
return "", fmt.Errorf("no phone number found")
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
logg.Warnf("failed to format phone number", "err", err)
|
||||||
|
return "", fmt.Errorf("failed to format number")
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
return nil, handlers.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
if err := rqv.ParseForm(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := rqv.FormValue("text")
|
||||||
|
|
||||||
|
parts := strings.Split(text, "*")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil, fmt.Errorf("no input found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(parts[len(parts)-1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQueryParams(query string) map[string]string {
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
queryParams := strings.Split(query, "&")
|
||||||
|
for _, param := range queryParams {
|
||||||
|
// Split each key-value pair by '='
|
||||||
|
parts := strings.SplitN(param, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
params[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractATSessionId(decodedStr string) (string, error) {
|
||||||
|
var data map[string]string
|
||||||
|
err := json.Unmarshal([]byte(decodedStr), &data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("Error unmarshalling JSON: %v", err)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
decodedBody, err := url.QueryUnescape(data["body"])
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("Error URL-decoding body: %v", err)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
params := parseQueryParams(decodedBody)
|
||||||
|
|
||||||
|
sessionId := params["sessionId"]
|
||||||
|
return sessionId, nil
|
||||||
|
|
||||||
|
}
|
@ -1,19 +1,25 @@
|
|||||||
package http
|
package at
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("atserver")
|
||||||
)
|
)
|
||||||
|
|
||||||
type ATSessionHandler struct {
|
type ATSessionHandler struct {
|
||||||
*SessionHandler
|
*httpserver.SessionHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||||
return &ATSessionHandler{
|
return &ATSessionHandler{
|
||||||
SessionHandler: ToSessionHandler(h),
|
SessionHandler: httpserver.ToSessionHandler(h),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,14 +37,14 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
cfg.SessionId, err = rp.GetSessionId(req)
|
cfg.SessionId, err = rp.GetSessionId(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
ash.writeError(w, 400, err)
|
ash.WriteError(w, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rqs.Config = cfg
|
rqs.Config = cfg
|
||||||
rqs.Input, err = rp.GetInput(req)
|
rqs.Input, err = rp.GetInput(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
ash.writeError(w, 400, err)
|
ash.WriteError(w, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
ash.writeError(w, 500, err)
|
ash.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
rqs, err = ash.Output(rqs)
|
rqs, err = ash.Output(rqs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ash.writeError(w, 500, err)
|
ash.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rqs, err = ash.Reset(rqs)
|
rqs, err = ash.Reset(rqs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ash.writeError(w, 500, err)
|
ash.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,4 +95,4 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque
|
|||||||
|
|
||||||
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
||||||
return rqs, err
|
return rqs, err
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package http
|
package at
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@ -16,16 +15,6 @@ import (
|
|||||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// invalidRequestType is a custom type to test invalid request scenarios
|
|
||||||
type invalidRequestType struct{}
|
|
||||||
|
|
||||||
// errorReader is a helper type that always returns an error when Read is called
|
|
||||||
type errorReader struct{}
|
|
||||||
|
|
||||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
|
||||||
return 0, errors.New("read error")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewATSessionHandler(t *testing.T) {
|
func TestNewATSessionHandler(t *testing.T) {
|
||||||
mockHandler := &httpmocks.MockRequestHandler{}
|
mockHandler := &httpmocks.MockRequestHandler{}
|
||||||
ash := NewATSessionHandler(mockHandler)
|
ash := NewATSessionHandler(mockHandler)
|
||||||
@ -242,208 +231,4 @@ func TestATSessionHandler_Output(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sessionID string
|
|
||||||
input []byte
|
|
||||||
parserErr error
|
|
||||||
processErr error
|
|
||||||
outputErr error
|
|
||||||
resetErr error
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Success",
|
|
||||||
sessionID: "123",
|
|
||||||
input: []byte("test input"),
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Missing Session ID",
|
|
||||||
sessionID: "",
|
|
||||||
parserErr: handlers.ErrSessionMissing,
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Process Error",
|
|
||||||
sessionID: "123",
|
|
||||||
input: []byte("test input"),
|
|
||||||
processErr: handlers.ErrStorage,
|
|
||||||
expectedStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Output Error",
|
|
||||||
sessionID: "123",
|
|
||||||
input: []byte("test input"),
|
|
||||||
outputErr: errors.New("output error"),
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Reset Error",
|
|
||||||
sessionID: "123",
|
|
||||||
input: []byte("test input"),
|
|
||||||
resetErr: errors.New("reset error"),
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockRequestParser := &httpmocks.MockRequestParser{
|
|
||||||
GetSessionIdFunc: func(any) (string, error) {
|
|
||||||
return tt.sessionID, tt.parserErr
|
|
||||||
},
|
|
||||||
GetInputFunc: func(any) ([]byte, error) {
|
|
||||||
return tt.input, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
|
||||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return rs, tt.processErr
|
|
||||||
},
|
|
||||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return rs, tt.outputErr
|
|
||||||
},
|
|
||||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
|
||||||
return rs, tt.resetErr
|
|
||||||
},
|
|
||||||
GetRequestParserFunc: func() handlers.RequestParser {
|
|
||||||
return mockRequestParser
|
|
||||||
},
|
|
||||||
GetConfigFunc: func() engine.Config {
|
|
||||||
return engine.Config{}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
|
||||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
sessionHandler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if status := rr.Code; status != tt.expectedStatus {
|
|
||||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
|
||||||
status, tt.expectedStatus)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSessionHandler_writeError(t *testing.T) {
|
|
||||||
handler := &SessionHandler{}
|
|
||||||
mockWriter := &httpmocks.MockWriter{}
|
|
||||||
err := errors.New("test error")
|
|
||||||
|
|
||||||
handler.writeError(mockWriter, http.StatusBadRequest, err)
|
|
||||||
|
|
||||||
if mockWriter.WrittenString != "" {
|
|
||||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
request any
|
|
||||||
expectedID string
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid Session ID",
|
|
||||||
request: func() *http.Request {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
req.Header.Set("X-Vise-Session", "123456")
|
|
||||||
return req
|
|
||||||
}(),
|
|
||||||
expectedID: "123456",
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Missing Session ID",
|
|
||||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
|
||||||
expectedID: "",
|
|
||||||
expectedError: handlers.ErrSessionMissing,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid Request Type",
|
|
||||||
request: invalidRequestType{},
|
|
||||||
expectedID: "",
|
|
||||||
expectedError: handlers.ErrInvalidRequest,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
parser := &DefaultRequestParser{}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
id, err := parser.GetSessionId(tt.request)
|
|
||||||
|
|
||||||
if id != tt.expectedID {
|
|
||||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != tt.expectedError {
|
|
||||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
request any
|
|
||||||
expectedInput []byte
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid Input",
|
|
||||||
request: func() *http.Request {
|
|
||||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
|
||||||
}(),
|
|
||||||
expectedInput: []byte("test input"),
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Input",
|
|
||||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
|
||||||
expectedInput: []byte{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid Request Type",
|
|
||||||
request: invalidRequestType{},
|
|
||||||
expectedInput: nil,
|
|
||||||
expectedError: handlers.ErrInvalidRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Read Error",
|
|
||||||
request: func() *http.Request {
|
|
||||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
|
||||||
}(),
|
|
||||||
expectedInput: nil,
|
|
||||||
expectedError: errors.New("read error"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
parser := &DefaultRequestParser{}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
input, err := parser.GetInput(tt.request)
|
|
||||||
|
|
||||||
if !bytes.Equal(input, tt.expectedInput) {
|
|
||||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
|
||||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
38
internal/http/parse.go
Normal file
38
internal/http/parse.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultRequestParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
return "", handlers.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
v := rqv.Header.Get("X-Vise-Session")
|
||||||
|
if v == "" {
|
||||||
|
return "", handlers.ErrSessionMissing
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
return nil, handlers.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
defer rqv.Body.Close()
|
||||||
|
v, err := ioutil.ReadAll(rqv.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -14,35 +13,6 @@ var (
|
|||||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||||
)
|
)
|
||||||
|
|
||||||
type DefaultRequestParser struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
|
||||||
rqv, ok := rq.(*http.Request)
|
|
||||||
if !ok {
|
|
||||||
return "", handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
v := rqv.Header.Get("X-Vise-Session")
|
|
||||||
if v == "" {
|
|
||||||
return "", handlers.ErrSessionMissing
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
|
||||||
rqv, ok := rq.(*http.Request)
|
|
||||||
if !ok {
|
|
||||||
return nil, handlers.ErrInvalidRequest
|
|
||||||
}
|
|
||||||
defer rqv.Body.Close()
|
|
||||||
v, err := ioutil.ReadAll(rqv.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionHandler struct {
|
type SessionHandler struct {
|
||||||
handlers.RequestHandler
|
handlers.RequestHandler
|
||||||
}
|
}
|
||||||
@ -53,25 +23,24 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
|
func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) {
|
||||||
s := err.Error()
|
s := err.Error()
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
_, err = w.Write([]byte{})
|
_, err = w.Write([]byte(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
var code int
|
var code int
|
||||||
var err error
|
var err error
|
||||||
var perr error
|
var perr error
|
||||||
|
|
||||||
rqs := handlers.RequestSession{
|
rqs := handlers.RequestSession{
|
||||||
Ctx: req.Context(),
|
Ctx: req.Context(),
|
||||||
Writer: w,
|
Writer: w,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,13 +49,13 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
cfg.SessionId, err = rp.GetSessionId(req)
|
cfg.SessionId, err = rp.GetSessionId(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
f.writeError(w, 400, err)
|
f.WriteError(w, 400, err)
|
||||||
}
|
}
|
||||||
rqs.Config = cfg
|
rqs.Config = cfg
|
||||||
rqs.Input, err = rp.GetInput(req)
|
rqs.Input, err = rp.GetInput(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
f.writeError(w, 400, err)
|
f.WriteError(w, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +72,7 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
f.writeError(w, 500, err)
|
f.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,11 +81,11 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
rqs, err = f.Output(rqs)
|
rqs, err = f.Output(rqs)
|
||||||
rqs, perr = f.Reset(rqs)
|
rqs, perr = f.Reset(rqs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.writeError(w, 500, err)
|
f.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
f.writeError(w, 500, perr)
|
f.WriteError(w, 500, perr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
229
internal/http/server_test.go
Normal file
229
internal/http/server_test.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// invalidRequestType is a custom type to test invalid request scenarios
|
||||||
|
type invalidRequestType struct{}
|
||||||
|
|
||||||
|
// errorReader is a helper type that always returns an error when Read is called
|
||||||
|
type errorReader struct{}
|
||||||
|
|
||||||
|
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||||
|
return 0, errors.New("read error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sessionID string
|
||||||
|
input []byte
|
||||||
|
parserErr error
|
||||||
|
processErr error
|
||||||
|
outputErr error
|
||||||
|
resetErr error
|
||||||
|
expectedStatus int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success",
|
||||||
|
sessionID: "123",
|
||||||
|
input: []byte("test input"),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing Session ID",
|
||||||
|
sessionID: "",
|
||||||
|
parserErr: handlers.ErrSessionMissing,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Process Error",
|
||||||
|
sessionID: "123",
|
||||||
|
input: []byte("test input"),
|
||||||
|
processErr: handlers.ErrStorage,
|
||||||
|
expectedStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Output Error",
|
||||||
|
sessionID: "123",
|
||||||
|
input: []byte("test input"),
|
||||||
|
outputErr: errors.New("output error"),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reset Error",
|
||||||
|
sessionID: "123",
|
||||||
|
input: []byte("test input"),
|
||||||
|
resetErr: errors.New("reset error"),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockRequestParser := &httpmocks.MockRequestParser{
|
||||||
|
GetSessionIdFunc: func(any) (string, error) {
|
||||||
|
return tt.sessionID, tt.parserErr
|
||||||
|
},
|
||||||
|
GetInputFunc: func(any) ([]byte, error) {
|
||||||
|
return tt.input, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||||
|
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||||
|
return rs, tt.processErr
|
||||||
|
},
|
||||||
|
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||||
|
return rs, tt.outputErr
|
||||||
|
},
|
||||||
|
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||||
|
return rs, tt.resetErr
|
||||||
|
},
|
||||||
|
GetRequestParserFunc: func() handlers.RequestParser {
|
||||||
|
return mockRequestParser
|
||||||
|
},
|
||||||
|
GetConfigFunc: func() engine.Config {
|
||||||
|
return engine.Config{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||||
|
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
sessionHandler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != tt.expectedStatus {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||||
|
status, tt.expectedStatus)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionHandler_WriteError(t *testing.T) {
|
||||||
|
handler := &SessionHandler{}
|
||||||
|
mockWriter := &httpmocks.MockWriter{}
|
||||||
|
err := errors.New("test error")
|
||||||
|
|
||||||
|
handler.WriteError(mockWriter, http.StatusBadRequest, err)
|
||||||
|
|
||||||
|
if mockWriter.WrittenString != "" {
|
||||||
|
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request any
|
||||||
|
expectedID string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Session ID",
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||||
|
req.Header.Set("X-Vise-Session", "123456")
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
expectedID: "123456",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing Session ID",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||||
|
expectedID: "",
|
||||||
|
expectedError: handlers.ErrSessionMissing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Request Type",
|
||||||
|
request: invalidRequestType{},
|
||||||
|
expectedID: "",
|
||||||
|
expectedError: handlers.ErrInvalidRequest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := &DefaultRequestParser{}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
id, err := parser.GetSessionId(tt.request)
|
||||||
|
|
||||||
|
if id != tt.expectedID {
|
||||||
|
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != tt.expectedError {
|
||||||
|
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request any
|
||||||
|
expectedInput []byte
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Input",
|
||||||
|
request: func() *http.Request {
|
||||||
|
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||||
|
}(),
|
||||||
|
expectedInput: []byte("test input"),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Input",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||||
|
expectedInput: []byte{},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Request Type",
|
||||||
|
request: invalidRequestType{},
|
||||||
|
expectedInput: nil,
|
||||||
|
expectedError: handlers.ErrInvalidRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Read Error",
|
||||||
|
request: func() *http.Request {
|
||||||
|
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||||
|
}(),
|
||||||
|
expectedInput: nil,
|
||||||
|
expectedError: errors.New("read error"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := &DefaultRequestParser{}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
input, err := parser.GetInput(tt.request)
|
||||||
|
|
||||||
|
if !bytes.Equal(input, tt.expectedInput) {
|
||||||
|
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||||
|
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,11 @@ import (
|
|||||||
"git.defalsify.org/vise.git/db"
|
"git.defalsify.org/vise.git/db"
|
||||||
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||||
"git.defalsify.org/vise.git/lang"
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("gdbmstorage")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
@ -13,6 +13,7 @@ import (
|
|||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.grassecon.net/urdt/ussd/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
|
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -75,7 +76,7 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D
|
|||||||
connStr := buildConnStr()
|
connStr := buildConnStr()
|
||||||
err = newDb.Connect(ctx, connStr)
|
err = newDb.Connect(ctx, connStr)
|
||||||
} else {
|
} else {
|
||||||
newDb = NewThreadGdbmDb()
|
newDb = gdbmstorage.NewThreadGdbmDb()
|
||||||
storeFile := path.Join(ms.dbDir, fileName)
|
storeFile := path.Join(ms.dbDir, fileName)
|
||||||
err = newDb.Connect(ctx, storeFile)
|
err = newDb.Connect(ctx, storeFile)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
var isoCodes = map[string]bool{
|
var isoCodes = map[string]bool{
|
||||||
"eng": true, // English
|
"eng": true, // English
|
||||||
"swa": true, // Swahili
|
"swa": true, // Swahili
|
||||||
|
"default": true, // Default language: English
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsValidISO639(code string) bool {
|
func IsValidISO639(code string) bool {
|
||||||
|
@ -62,10 +62,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1234",
|
"input": "1234",
|
||||||
"expectedContent": "Select language:\n0:English\n1:Kiswahili"
|
"expectedContent": "Select language:\n1:English\n2:Kiswahili"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "1",
|
||||||
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -430,7 +430,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1234",
|
"input": "1234",
|
||||||
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 84\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
|
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "0",
|
||||||
|
@ -298,9 +298,10 @@ func TestMainMenuSend(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
sessions := testData
|
sessions := testData
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
|
groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
for _, step := range group.Steps {
|
for index, step := range group.Steps {
|
||||||
|
t.Logf("step %v with input %v", index, step.Input)
|
||||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
@ -7,14 +7,14 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"input": "",
|
"input": "",
|
||||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
|
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "1",
|
||||||
"expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
|
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "1",
|
||||||
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
|
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -40,14 +40,14 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"input": "",
|
"input": "",
|
||||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
|
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": "0",
|
|
||||||
"expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1",
|
"input": "1",
|
||||||
|
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -64,8 +64,8 @@
|
|||||||
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "000",
|
"input": "0@0",
|
||||||
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
|
"expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1",
|
"input": "1",
|
||||||
|
@ -7,3 +7,4 @@ HALT
|
|||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP my_balance 1
|
INCMP my_balance 1
|
||||||
INCMP community_balance 2
|
INCMP community_balance 2
|
||||||
|
INCMP . *
|
||||||
|
@ -2,9 +2,9 @@ LOAD reset_account_authorized 0
|
|||||||
LOAD reset_incorrect 0
|
LOAD reset_incorrect 0
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH pin_entry flag_account_authorized 0
|
CATCH pin_entry flag_account_authorized 0
|
||||||
MOUT english 0
|
MOUT english 1
|
||||||
MOUT kiswahili 1
|
MOUT kiswahili 2
|
||||||
HALT
|
HALT
|
||||||
INCMP set_default 0
|
INCMP set_eng 1
|
||||||
INCMP set_swa 1
|
INCMP set_swa 2
|
||||||
INCMP . *
|
INCMP . *
|
||||||
|
@ -9,3 +9,4 @@ MOUT quit 9
|
|||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP quit 9
|
INCMP quit 9
|
||||||
|
INCMP . *
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
Jina la kwanza la sasa {{.get_current_profile_info}}
|
Jina la kwanza la sasa: {{.get_current_profile_info}}
|
||||||
Weka majina yako ya kwanza:
|
Weka majina yako ya kwanza:
|
@ -1,2 +1,2 @@
|
|||||||
Eneo la sasa {{.get_current_profile_info}}
|
Eneo la sasa: {{.get_current_profile_info}}
|
||||||
Weka eneo:
|
Weka eneo:
|
@ -10,5 +10,4 @@ CATCH _ flag_back_set 1
|
|||||||
RELOAD save_offerings
|
RELOAD save_offerings
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
CATCH pin_entry flag_offerings_set 1
|
CATCH pin_entry flag_offerings_set 1
|
||||||
CATCH pin_entry flag_offerings_set 0
|
|
||||||
INCMP update_profile_items *
|
INCMP update_profile_items *
|
||||||
|
@ -20,3 +20,4 @@ INCMP edit_yob 4
|
|||||||
INCMP edit_location 5
|
INCMP edit_location 5
|
||||||
INCMP edit_offerings 6
|
INCMP edit_offerings 6
|
||||||
INCMP view_profile 7
|
INCMP view_profile 7
|
||||||
|
INCMP . *
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}}
|
Mwaka wa sasa wa kuzaliwa: {{.get_current_profile_info}}
|
||||||
Weka mwaka wa kuzaliwa
|
Weka mwaka wa kuzaliwa
|
@ -14,3 +14,4 @@ INCMP balances 3
|
|||||||
INCMP check_statement 4
|
INCMP check_statement 4
|
||||||
INCMP pin_management 5
|
INCMP pin_management 5
|
||||||
INCMP address 6
|
INCMP address 6
|
||||||
|
INCMP . *
|
||||||
|
@ -9,3 +9,4 @@ MOUT quit 9
|
|||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP quit 9
|
INCMP quit 9
|
||||||
|
INCMP . *
|
||||||
|
@ -11,3 +11,4 @@ INCMP _ 0
|
|||||||
INCMP set_male 1
|
INCMP set_male 1
|
||||||
INCMP set_female 2
|
INCMP set_female 2
|
||||||
INCMP set_unspecified 3
|
INCMP set_unspecified 3
|
||||||
|
INCMP . *
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
Jinsia ya sasa {{.get_current_profile_info}}
|
Jinsia ya sasa: {{.get_current_profile_info}}
|
||||||
Chagua jinsia
|
Chagua jinsia
|
@ -1,6 +1,6 @@
|
|||||||
MOUT english 0
|
MOUT english 1
|
||||||
MOUT kiswahili 1
|
MOUT kiswahili 2
|
||||||
HALT
|
HALT
|
||||||
INCMP set_eng 0
|
INCMP set_eng 1
|
||||||
INCMP set_swa 1
|
INCMP set_swa 2
|
||||||
INCMP . *
|
INCMP . *
|
||||||
|
4
services/registration/set_default.vis
Normal file
4
services/registration/set_default.vis
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
LOAD set_language 6
|
||||||
|
RELOAD set_language
|
||||||
|
CATCH terms flag_account_created 0
|
||||||
|
MOVE language_changed
|
@ -1 +1,2 @@
|
|||||||
Do you agree to terms and conditions?
|
Do you agree to terms and conditions?
|
||||||
|
https://grassecon.org/pages/terms-and-conditions
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
MOUT yes 0
|
MOUT yes 1
|
||||||
MOUT no 1
|
MOUT no 2
|
||||||
HALT
|
HALT
|
||||||
INCMP create_pin 0
|
INCMP create_pin 1
|
||||||
INCMP quit *
|
INCMP quit *
|
||||||
|
@ -1 +1,2 @@
|
|||||||
Kwa kutumia hii huduma umekubali sheria na masharti?
|
Kwa kutumia hii huduma umekubali sheria na masharti?
|
||||||
|
https://grassecon.org/pages/terms-and-conditions
|
||||||
|
@ -4,5 +4,8 @@ LOAD reset_incorrect 6
|
|||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH pin_entry flag_account_authorized 0
|
CATCH pin_entry flag_account_authorized 0
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
|
MOUT quit 9
|
||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
|
INCMP quit 9
|
||||||
|
INCMP . *
|
||||||
|
Loading…
Reference in New Issue
Block a user