Compare commits

...

462 Commits

Author SHA1 Message Date
0bb444cd50 Merge pull request 'updated README' (#169) from readme-documentation into master
Reviewed-on: #169
2024-11-08 22:23:57 +01:00
1d07d7fb1d
updated README 2024-11-08 20:35:17 +03:00
a3e5aab6c4 Merge pull request 'Africastalking POST route' (#168) from africastalking-endpoint into master
Reviewed-on: #168
2024-11-08 17:25:46 +01:00
574807d254
set the africastalking POST route using env 2024-11-08 16:52:19 +03:00
256ed6491b Merge pull request 'http-logs' (#167) from http-logs into master
Reviewed-on: #167
2024-11-08 12:51:30 +01:00
7a02ffcf0c Merge pull request 'log-file' (#166) from log-file into http-logs
Reviewed-on: #166
2024-11-08 08:09:15 +01:00
dcd8fce59a
add log on create account 2024-11-08 10:07:06 +03:00
64a7b49218
Remove unused Warning logger 2024-11-08 00:21:11 +03:00
1bcbb2079e
Removed unused variable 2024-11-08 00:17:02 +03:00
e63468433e
Merge branch 'http-logs' into log-file 2024-11-08 00:14:34 +03:00
9c972ffa6b
add specific log files per binary 2024-11-07 16:46:12 +03:00
a11776e1b3
ignore .log files 2024-11-07 16:41:38 +03:00
6ac9ac29d8
add simple http logging 2024-11-07 16:41:08 +03:00
46b2b354fd Merge pull request 'swahili-templates-menu' (#158) from swahili-templates-menu into master
Reviewed-on: #158
Reviewed-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-11-05 11:29:07 +01:00
7676cfd40c
Merge branch 'master' into swahili-templates-menu 2024-11-05 13:25:49 +03:00
4e350aa25a
update test data file 2024-11-05 10:49:08 +03:00
859de0513a Merge pull request 'api-error-fix' (#161) from api-error-fix into master
Reviewed-on: #161
2024-11-05 00:18:04 +01:00
266d3d06c3
Check the flag_no_active_voucher before proceeding 2024-11-04 17:56:25 +03:00
92ea3df4aa
Removeearly return statement 2024-11-04 17:38:55 +03:00
c46c31ea36
ensure swahili translation 2024-11-04 16:06:33 +03:00
da91eed9d4
add retry menu option 2024-11-04 15:55:58 +03:00
e2b28a31b2 Merge pull request 'lash/export-to-term' (#157) from lash/export-to-term into master
Reviewed-on: #157
2024-11-04 13:54:58 +01:00
88b50c5dd7
Remove admin number defination in env example 2024-11-04 15:52:03 +03:00
43a1208cce
Merge branch 'master' into lash/export-to-term 2024-11-04 15:38:53 +03:00
2b865a365b
add back option to view address 2024-11-04 15:24:51 +03:00
c77558689a
ensure swahlili menu template 2024-11-04 15:24:22 +03:00
a9641fd70d
ensure swahili menu template 2024-11-04 15:24:01 +03:00
lash
2c30ccc405
Add voucherdata call to test accountservices 2024-11-04 02:23:30 +00:00
lash
7189235bee
Remove unused data type 2024-11-03 15:14:17 +00:00
lash
0506a8c452
Add voucherdata endpoint 2024-11-03 14:34:26 +00:00
lash
a237b615f2
Export models package 2024-11-03 01:44:57 +00:00
lash
dae12ac498
Separate subprefix db export 2024-11-02 23:41:08 +00:00
lash
1d77ad98dc
Expose subprefix db 2024-11-02 23:33:52 +00:00
lash
3a8a5f40ba
Add test service placeholders for fetchtransactions 2024-11-02 16:42:50 +00:00
lash
14bc11f4bd
Add transaction getter 2024-11-02 16:38:29 +00:00
lash
e29a24b376
Add missing models files 2024-11-02 15:46:46 +00:00
lash
35a090ef42 Merge branch 'pre-mock-remove' into lash/export-to-term 2024-11-02 14:00:16 +00:00
2587882eae Merge pull request 'pin-reset' (#139) from pin-reset into pre-mock-remove
Reviewed-on: #139
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-11-02 14:54:19 +01:00
24d4b8478e Merge pull request 'Remove db mocks' (#152) from remove-db-mocks into master
Reviewed-on: #152
2024-11-02 13:44:03 +01:00
476a69fe1b
Update menuhandler tests to use the memdb instead of dbmocks 2024-11-02 03:22:33 +03:00
c52f8312e4
Remove the dbmock and userdbmock 2024-11-02 03:21:38 +03:00
6dbe74d12b
use single temporary value 2024-11-01 16:46:09 +03:00
332074375a
wrap in devtools/admin 2024-11-01 16:44:54 +03:00
5e4a9e7567
Merge branch 'master' into pin-reset 2024-11-01 16:26:29 +03:00
cfe3e526df
Remove the subprefixdbmock 2024-11-01 13:17:45 +03:00
eb2c73dce1
remove unused setadmin store 2024-11-01 11:51:50 +03:00
7e448f739a
change fs store path to root 2024-11-01 09:35:48 +03:00
0014693ba8
remove guard pin option 2024-11-01 06:39:37 +03:00
lash
9a528cfd14
Clean up messily failed conflict resolution 2024-11-01 03:17:01 +00:00
lash
8cc46d2782 Merge branch 'master' into lash/export-to-term 2024-10-31 22:58:36 +00:00
45945ae9c5 Merge pull request 'Consolidate temp data storage' (#150) from consolidate-temp-data-storage into master
Reviewed-on: #150
2024-10-31 23:47:04 +01:00
d7ea8fa651
Use the DATA_TEMPORARY_VALUE for the user data 2024-11-01 01:10:58 +03:00
e75aa2c1f7 Merge branch 'master' into consolidate-temp-data-storage 2024-11-01 00:45:32 +03:00
63eed81d3d
Merge branch 'master' into consolidate-temp-data-storage 2024-11-01 00:44:15 +03:00
f4ca4454ea
use a single DATA_TEMPORARY_VALUE for the PIN and voucher data 2024-11-01 00:42:23 +03:00
2704069e74
Merge branch 'master' into pin-reset 2024-10-31 21:09:48 +03:00
7d1a04f089
remove from root 2024-10-31 21:01:24 +03:00
53fa6f64ce
define structure of json 2024-10-31 21:01:01 +03:00
7fa38340dd
add command to initialize a list of admin numbers 2024-10-31 21:00:41 +03:00
7aab3cff8c
remove seed 2024-10-31 21:00:16 +03:00
299534ccf1
define seed as a command in the devtool 2024-10-31 20:59:51 +03:00
b2655b7f11
remove seed from executable 2024-10-31 20:59:11 +03:00
5abe9b78cc
attach an admin store for the phone numbers 2024-10-31 20:11:26 +03:00
12825ae08a
setup adminstore in the local handler service 2024-10-31 20:10:46 +03:00
ac0b4b2ed1
pass context 2024-10-31 20:08:30 +03:00
5f666382ab Merge pull request 'profile-update-pin-check' (#149) from profile-update-pin-check into master
Reviewed-on: #149
2024-10-31 16:06:47 +01:00
ce917d9e89
update tests 2024-10-31 17:05:06 +03:00
3ce25d0e14
Merge branch 'master' into profile-update-pin-check 2024-10-31 16:42:30 +03:00
8fe8ff540b
Merge branch 'master' into pin-reset 2024-10-31 16:38:28 +03:00
0b4bf58107
resolved failing tests due to tempData 2024-10-31 16:31:29 +03:00
lash
4bf56c525f
Export voucher related code 2024-10-31 13:19:44 +00:00
lash
33bba73a65 Merge branch 'master' into lash/export-to-term 2024-10-31 12:45:53 +00:00
074345fcf9 Merge pull request 'voucher-data' (#138) from voucher-data into master
Reviewed-on: #138
2024-10-31 13:44:18 +01:00
lash
a17150962e Merge remote-tracking branch 'origin/lash/export-to-term' into lash/export-to-term 2024-10-31 12:39:48 +00:00
lash
3ae75b27a5
WIP replaced check account method but traversal crashes 2024-10-31 12:38:31 +00:00
lash
b2d180e8eb
WIP Trying to clean up account status check 2024-10-31 12:15:07 +00:00
e6a369dcdd
remove unused code 2024-10-31 14:44:42 +03:00
lash
b9c56b04ce
Remove obsolete track account status code 2024-10-31 11:43:27 +00:00
b8bbd88078
retain the temporary data for it to be overwritten 2024-10-31 14:26:28 +03:00
d25128287e
update profile information on individual node 2024-10-31 14:21:22 +03:00
c45fcda2f1
remove back option check,protect profile data update 2024-10-31 14:20:04 +03:00
211cc1f775
perform profile update in individual update node 2024-10-31 14:19:12 +03:00
981f7ca4f6
add keys to for holding temporary profile info 2024-10-31 14:14:50 +03:00
05ed236e03
add node to update individual profile information 2024-10-31 14:14:10 +03:00
bab3f673eb
Removed unused model 2024-10-31 14:13:21 +03:00
767a3cd64c
remove pin guard menu option 2024-10-31 09:38:51 +03:00
c4078c5280
remove extra spaces 2024-10-31 09:21:46 +03:00
lash
dc198215b1
Remove commented code 2024-10-31 02:03:29 +00:00
lash
a48170321c
Finish refactor result models 2024-10-31 01:51:36 +00:00
lash
1e638238ed
WIP refactor models usage 2024-10-31 01:28:37 +00:00
lash
4e81e2d869
Adapt voucher changes to package exports 2024-10-30 21:09:45 +00:00
d434194021
catch incorrect pin entry 2024-10-30 23:21:15 +03:00
c6ca3f6be4
fix sink error 2024-10-30 22:32:38 +03:00
8093eae61a
use 0 instead of 1 for back 2024-10-30 21:26:52 +03:00
833d52a558
remove print message 2024-10-30 21:26:11 +03:00
8262e14198
Merge branch 'master' into pin-reset 2024-10-30 21:10:12 +03:00
a31cac4e50
Merge branch 'master' into voucher-data 2024-10-30 18:46:21 +03:00
8b399781e8
make the VoucherMetadata more descriptive 2024-10-30 18:40:03 +03:00
caafe495be
use the dataserviceapi structs 2024-10-30 18:30:55 +03:00
66b34eaea4
get the ussd-data-service package 2024-10-30 18:21:49 +03:00
ea4c6d9314
check for phone number validity 2024-10-30 18:01:43 +03:00
7c823e07ca
move to root node after on back 2024-10-30 18:01:20 +03:00
41585f831c
move catch and load to next node 2024-10-30 18:00:38 +03:00
d93a26f9b0
remove function exec after HALT 2024-10-30 17:58:52 +03:00
lash
ff26ccc545
Expose api interface 2024-10-30 13:09:15 +00:00
lash
72c688b885 Merge branch 'master' into lash/export-to-term 2024-10-30 12:02:37 +00:00
lash
14648fec6c
Edit comment 2024-10-30 12:02:16 +00:00
cf523e30f8
update handler in test 2024-10-30 14:50:31 +03:00
888d3befe9
add actual pin reset functionality 2024-10-30 14:50:12 +03:00
017691a40c
define structure for admin numbers 2024-10-30 14:41:14 +03:00
dc418771a7
attach an admin store for phone numbers 2024-10-30 14:33:48 +03:00
c2068db050
update handler functions 2024-10-30 14:33:22 +03:00
c42b1cd66b
provide required handler functions and admin store 2024-10-30 14:32:01 +03:00
b404ae95fb
setup an admin store based on fs 2024-10-30 14:30:38 +03:00
adaa0c63ef
Extract common db operations for the test 2024-10-30 14:12:42 +03:00
843b0d1e7e Merge pull request 'Add reverse sessionid address lookup' (#146) from lash/reverse-session into master
Reviewed-on: #146
2024-10-30 10:35:55 +01:00
lash
c95b97cb14
Export user data store 2024-10-30 01:45:38 +00:00
lash
dd764a2e24
Export db datatypes,tools 2024-10-30 01:28:55 +00:00
lash
cb997159f7
Add reverse sessionid address lookup 2024-10-30 00:59:59 +00:00
7241cdbfcb
Updated the tests 2024-10-30 00:53:13 +03:00
0480c02633
Moved voucher related functions to the utils package 2024-10-30 00:52:23 +03:00
0a97f610a4
catch unregistred number 2024-10-29 22:23:22 +03:00
5a0563df94
group regex,check for valid number against the regex 2024-10-29 22:17:43 +03:00
7597b96dae
remove catch for unregistered number 2024-10-29 22:16:34 +03:00
f37483e2f0
use _ for back navigation 2024-10-29 22:15:31 +03:00
d0ad6395b5
add check for unregistered phone numbers 2024-10-29 17:35:42 +03:00
106983a394
use explicit back to node 2024-10-29 17:35:01 +03:00
91b85af11a
add reset unregistered number 2024-10-29 17:34:34 +03:00
534d756318
catch unregistred phone numbers 2024-10-29 17:18:39 +03:00
6998c30dd1
add node to handle unregistered phone numbers 2024-10-29 17:18:01 +03:00
449f90c95b
add flag to catch unregistred numbers 2024-10-29 17:17:03 +03:00
e96c874300
repeat same node on invalid option input 2024-10-29 15:02:40 +03:00
b35460d3c1
Merge branch 'master' into pin-reset 2024-10-29 14:35:17 +03:00
124049c924
add admin number defination in env 2024-10-29 14:32:17 +03:00
5fd3eb3c29
set admin privilege flag 2024-10-29 14:28:58 +03:00
d83962c0ba
load admin numbers defined in the .env 2024-10-29 14:26:24 +03:00
41da099933
remove the admin flag,setup an admin store 2024-10-29 13:24:13 +03:00
c9bb93ede6
create a simple admin store for phone numbers 2024-10-29 13:15:41 +03:00
ca13d9155c
replace _ with explicit back node 2024-10-29 13:15:22 +03:00
e338ce0025
load and reload only after input 2024-10-29 13:14:49 +03:00
03c32fd265 Merge branch 'master' into voucher-data 2024-10-28 18:40:37 +03:00
727f54ee57 Merge pull request 'tests-refactor' (#137) from tests-refactor into master
Reviewed-on: #137
2024-10-28 16:28:41 +01:00
b97965193b
add pin reset for others handling 2024-10-28 16:37:23 +03:00
aec0abb2b6
setup an admin flag 2024-10-28 16:34:33 +03:00
2c361e5b96
Added tests related to the vouchers list 2024-10-28 16:30:13 +03:00
131f3bcf46
Added the prefixDb to the Handlers struct~ 2024-10-28 16:27:24 +03:00
520f5abdcd
Added the subPrefixDb mock 2024-10-28 16:23:44 +03:00
5d294b663c
Added the PrefixDb interface 2024-10-28 16:21:31 +03:00
26073c8000
define handler functions required to reset others pin 2024-10-28 15:49:20 +03:00
e4c2f644f3
define a key to hold number during pin reset 2024-10-28 15:47:56 +03:00
3de46cef5e
setup pin reset nodes 2024-10-28 15:45:08 +03:00
0cc0bdf9f7
add pin reset for others nodes 2024-10-28 15:19:35 +03:00
72d5c186dd
add admin privilege flag 2024-10-28 15:18:40 +03:00
dc782d87a8
Updated the TestSetVoucher 2024-10-28 11:20:06 +03:00
ddae746b9d
formatted code 2024-10-28 11:07:59 +03:00
4e1b2d5ddb
update account services 2024-10-25 18:01:52 +03:00
d8800a665d
Merge branch 'master' into tests-refactor 2024-10-25 17:48:40 +03:00
6c3ff0e9db Merge branch 'master' into voucher-data 2024-10-25 17:37:08 +03:00
3ef64271e7
Store and retrieve the voucher decimals and contract address 2024-10-25 17:24:09 +03:00
2dee47404d Merge pull request 'menu-voucherlist' (#101) from menu-voucherlist into master
Reviewed-on: #101
2024-10-25 15:59:46 +02:00
3792bfdc1f
run mod tidy 2024-10-25 09:38:09 +03:00
a0e97cfe5b
remove test account service implementation 2024-10-25 09:37:16 +03:00
a3be98c514
update test account service 2024-10-25 09:36:43 +03:00
2b34b3a75c
Merge branch 'master' into tests-refactor 2024-10-25 09:14:16 +03:00
b4454f7517
Added context to FetchVouchers 2024-10-24 20:44:29 +03:00
d9c660b8ea
Merge branch 'master' into menu-voucherlist 2024-10-24 20:39:43 +03:00
d113ea82fd
Add dynamic send_amount and session_id 2024-10-24 20:21:28 +03:00
a92c640cb7
Updated tests 2024-10-24 18:15:54 +03:00
728815f0c6
Include the active symbol in the send menu 2024-10-24 17:50:37 +03:00
6b5d3f74d1 Merge pull request 'api-context' (#127) from api-context into master
Reviewed-on: #127
2024-10-24 16:45:37 +02:00
bee9ad5ff5
Merge remote-tracking branch 'refs/remotes/origin/api-context' into api-context 2024-10-24 17:39:11 +03:00
6e7b46666e
ensure mod match with master 2024-10-24 17:34:42 +03:00
383f074cae
update method signatures 2024-10-24 17:32:08 +03:00
d678a639b8
Merge branch 'master' into api-context 2024-10-24 17:07:11 +03:00
453fea569a Merge pull request 'api-structs' (#117) from api-structs into master
Reviewed-on: #117
2024-10-24 15:53:46 +02:00
5c75e35fe0 Delete coverage.html
delete cover.html
2024-10-24 15:45:52 +02:00
cff50538fa Delete cover.out
delete cover.out
2024-10-24 15:45:29 +02:00
69a4530269 Delete services/registration/locale/swa/default.mo
remove dev file
2024-10-24 15:41:47 +02:00
db19e38717
Merge remote-tracking branch 'refs/remotes/origin/api-structs' into api-structs 2024-10-24 16:37:07 +03:00
c796bbdcfc
correct create endpoint 2024-10-24 16:36:09 +03:00
2b34a0900c
check for status code and unmarshal on errResponse 2024-10-24 16:35:53 +03:00
e15bac98a5 Merge branch 'master' into menu-voucherlist 2024-10-24 16:31:49 +03:00
ee9a683eb0
Merge branch 'master' into menu-voucherlist 2024-10-24 16:30:44 +03:00
9274e585bd
Merge branch 'master' into menu-voucherlist 2024-10-24 16:26:32 +03:00
57a49819f4
Merge branch 'master' into api-structs 2024-10-24 16:22:41 +03:00
abc01b7cee
change to local setup endpoint 2024-10-24 16:21:34 +03:00
d19c20a9d7 Merge branch 'master' into api-structs 2024-10-24 15:19:06 +02:00
0c6144d262 Merge pull request 'send-menu-update' (#131) from send-menu-update into master
Reviewed-on: #131
2024-10-24 15:15:11 +02:00
ec4e44a27c Merge branch 'master' into send-menu-update 2024-10-24 15:58:59 +03:00
69eb57f794 Merge branch 'master' into api-context 2024-10-24 14:51:25 +02:00
2347d64acc Merge pull request 'check-balance-update' (#132) from check-balance-update into master
Reviewed-on: #132
2024-10-24 14:50:59 +02:00
39c0560abe
Updated the test 2024-10-24 15:21:48 +03:00
75459f852b
Return the full balance string 2024-10-24 15:12:57 +03:00
6200728435
Updated the menuhander test 2024-10-24 14:45:15 +03:00
579b46db65
Replace the public key with the sessionId 2024-10-24 14:34:12 +03:00
f99486c190
correct import 2024-10-24 12:07:00 +03:00
57a07af8ca
correct import 2024-10-24 12:06:44 +03:00
5692440099
move to testutil 2024-10-24 12:05:27 +03:00
651969668f
move to testutil 2024-10-24 12:05:11 +03:00
f3a028f1fc
move to testutil 2024-10-24 12:03:32 +03:00
9f2d57ea03
update tests 2024-10-24 10:10:14 +03:00
d74a4cc33b
update service mock method signatures 2024-10-24 10:10:03 +03:00
308ca99fb0
remove reload account creation 2024-10-24 10:02:32 +03:00
08e709f1b3
check for error responses 2024-10-24 10:02:15 +03:00
9bc9d04a49
use errresponse and okresponse for deserialization 2024-10-24 10:00:38 +03:00
a553731f02
Cleaned up code 2024-10-23 17:45:41 +03:00
4011597d9c
Check specific db error 2024-10-23 14:02:13 +03:00
176473aa26
Rename prefix to vouchers 2024-10-23 13:54:42 +03:00
b41e52af63
pass context.Context 2024-10-23 12:45:54 +03:00
fb32dde136
run go mod tidy 2024-10-23 12:45:10 +03:00
1e6cf6a33a
run mod tidy 2024-10-23 11:08:51 +03:00
2725323f16
Merge branch 'master' into tests-refactor 2024-10-23 11:06:10 +03:00
5f1ee396d8
Fix PIN being requested twice 2024-10-23 06:35:19 +03:00
5f2c6cce16
add required track endpoint 2024-10-22 21:36:28 +03:00
3179ec1f62
add local track endpoint 2024-10-22 21:35:52 +03:00
b9a63f3c6f
merge dep 2024-10-22 21:30:48 +03:00
1d255372b2
Merge branch 'master' into api-structs 2024-10-22 21:05:51 +03:00
306666a15e
load and reload after halt 2024-10-22 20:37:30 +03:00
e05dbb4885
check for back option 2024-10-22 20:36:58 +03:00
637e107525
Merge branch 'master' into menu-voucherlist 2024-10-22 17:10:51 +03:00
59d0446020 Merge pull request 'postgres-switch' (#113) from postgres-switch into master
Reviewed-on: #113
2024-10-22 16:04:56 +02:00
f37ec13c75
polish code 2024-10-22 16:31:29 +03:00
0547bc7e5f
added send menu test with dynamic max_amount 2024-10-22 14:24:22 +03:00
02cb75f97a
increase the size to resolve sink error 2024-10-21 17:14:59 +03:00
9c75942b0b
chore: remove commented code 2024-10-21 16:54:16 +03:00
5909659fa9
Use dynamic balance 2024-10-21 16:52:07 +03:00
eaac771722
move into testtag package 2024-10-21 16:48:24 +03:00
f3a5178de7
update imports 2024-10-21 16:47:43 +03:00
549d09890b
update imports 2024-10-21 16:47:25 +03:00
4a9ef6b5f2
move test account service to testutil 2024-10-21 16:47:00 +03:00
961d8d1a5c
update package import 2024-10-21 16:46:29 +03:00
a904cdbf44
move driver to testutil 2024-10-21 16:45:06 +03:00
3af943f77c
move account service tags switch to test tags package 2024-10-21 16:44:02 +03:00
14455f65cb
move account service to testutil 2024-10-21 16:43:24 +03:00
0c08654df3
move driver to testutil 2024-10-21 16:42:57 +03:00
acd0af25c6
catch api call error 2024-10-21 11:11:04 +03:00
66d2e2e838
add custodial api structs 2024-10-21 11:10:42 +03:00
4beeb9986a
use importable api structs 2024-10-21 11:10:23 +03:00
d81bc0eefb
use importable api structs 2024-10-21 11:10:01 +03:00
5b0a383513
use importable api structs 2024-10-21 11:09:37 +03:00
367d3f0593
remove manually added api.go file 2024-10-21 11:07:32 +03:00
415c807464
include the Database in context 2024-10-21 09:22:07 +03:00
9f562fe53e
use postgres directly from go-vise 2024-10-19 23:06:58 +03:00
3bf2045f15
added FetchVouchers to the TestAccountService 2024-10-19 16:07:40 +03:00
aced3e4fc3 Merge branch 'master' into menu-voucherlist 2024-10-19 15:38:54 +03:00
fb0d2db156 Merge branch 'master' into postgres-switch 2024-10-19 15:28:59 +03:00
a40fc37da4
implement postgres for the state store 2024-10-19 15:27:23 +03:00
f13f5996c1
remove thread abstraction from postgres 2024-10-19 14:51:41 +03:00
00a2beae50
rename file 2024-10-19 13:41:19 +03:00
25bc7006a4
show a balance of 0 and prevent sending when no voucher exists 2024-10-18 18:31:40 +03:00
f643aa4d14
update tests 2024-10-18 17:37:04 +03:00
b8860478b6
implement expected structs 2024-10-18 17:36:51 +03:00
847b91ca9e
Merge branch 'menu-api-errors' into api-structs 2024-10-18 17:15:05 +03:00
353e24de33
define api structs 2024-10-18 17:04:43 +03:00
1c57c95d93
use api structs 2024-10-18 17:04:02 +03:00
128a354b34
use api structs 2024-10-18 17:03:48 +03:00
81c4189c8e
update tests 2024-10-18 17:02:08 +03:00
b8e12e5215
update api endpoints 2024-10-18 17:01:21 +03:00
113f1a5b34 Merge pull request 'menu-traversal-v2' (#115) from menu-traversal-v2 into master
Reviewed-on: #115
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-10-18 15:38:56 +02:00
4f04362835
pass error description to error response 2024-10-17 16:08:18 +03:00
fd8bfae8c7
updated the vise version to include the updated column 2024-10-17 15:35:35 +03:00
eea3be3a39
resolve failing test 2024-10-17 13:49:56 +03:00
849a950fbc Merge branch 'master' into menu-voucherlist 2024-10-17 13:44:31 +03:00
6727bd3769
polished code 2024-10-17 12:55:47 +03:00
9f8fcf1ed0
attach account service to handler 2024-10-17 12:54:11 +03:00
4a599b902d
define transaction 2024-10-17 12:49:51 +03:00
a759f47c8e Merge branch 'master' into postgres-switch 2024-10-17 12:49:28 +03:00
d181c34946
update handler and move transaction struct to models 2024-10-17 12:49:28 +03:00
4968cdff37
switch to postgres once the flag is set 2024-10-17 12:47:57 +03:00
bfa6eac4c2
define a test account service 2024-10-17 12:47:44 +03:00
1aeb18379c
added a db flag and Database to the context 2024-10-17 12:46:20 +03:00
667a21d950
pass account service as a param 2024-10-17 12:37:15 +03:00
4416c008fc
add menu traversal tests 2024-10-17 12:36:24 +03:00
0a7389a71d
explicitly reset authorize flag 2024-10-17 12:36:02 +03:00
383e64776d
chore: change pin to PIN 2024-10-17 12:35:44 +03:00
a27c1790b8
setup test data file 2024-10-17 12:34:17 +03:00
f81e3508ca
define engine to run tests 2024-10-17 12:33:38 +03:00
127219f510
define build for running online and offline tests 2024-10-17 12:33:09 +03:00
54cc33c819
Delete connstr in threadgdbm global channel map on close 2024-10-17 12:32:31 +03:00
a51f739d06
pass account service as a param 2024-10-17 12:31:33 +03:00
8d047ebe05
define universal group driver 2024-10-17 12:30:58 +03:00
4e840ac17c
add uuid 2024-10-17 12:30:36 +03:00
f73b7a8b04
setup api responses 2024-10-16 22:40:40 +03:00
e986eaa538 Merge pull request 'menu-api-errors' (#112) from menu-api-errors into master
Reviewed-on: #112
2024-10-16 19:08:46 +02:00
d8db8df643
use go-vise v0.2.0 2024-10-15 23:42:39 +03:00
09eac03e10
use env variables 2024-10-15 23:41:16 +03:00
51122d0fc5
added an env.example file 2024-10-15 22:31:37 +03:00
d05c666513 Merge branch 'master' into menu-voucherlist 2024-10-15 17:08:48 +03:00
a336856c9b
use separate functions to process voucher data 2024-10-15 17:02:51 +03:00
0be570ae2d
add check for api call failure 2024-10-15 16:31:31 +03:00
6a36bc43b5
add check for api call failure 2024-10-15 16:31:15 +03:00
1d27a88908
add test on validate amount 2024-10-15 16:30:29 +03:00
f378f15422
updated tests 2024-10-15 16:29:51 +03:00
b2fb9faf6c
use the active balance to validate the amount 2024-10-15 16:17:33 +03:00
859e203a00
removed debug comments 2024-10-15 15:54:43 +03:00
6c6af5ec21
set a default voucher as the active sym if none exists 2024-10-15 15:46:02 +03:00
8ed98b3e6c
resolve case issue 2024-10-15 14:53:00 +03:00
4889e6d18b
Merge remote-tracking branch 'origin' into menu-api-errors 2024-10-15 14:04:45 +03:00
368c25125a
set flag count to 128 2024-10-15 13:59:49 +03:00
283793a2ae
add swahili menu option 2024-10-15 13:57:45 +03:00
bec7e5c69f
update: fetch balances in a sepate function,show profile information in swahili and english 2024-10-15 13:57:05 +03:00
d638aba85e
update tests 2024-10-15 13:50:28 +03:00
df7788dd0b
return actual reponses on the api calls 2024-10-15 13:48:14 +03:00
4a62773098
add handler fetching custodial balances 2024-10-15 13:44:50 +03:00
26d315b032
add check for api call failure 2024-10-15 13:42:20 +03:00
1927544533
reset authorized flag 2024-10-15 13:40:40 +03:00
c641a0c669
add api failure nodes 2024-10-14 23:19:09 +03:00
952da86931
update balance nodes 2024-10-14 23:18:42 +03:00
be6391686f
update return type 2024-10-14 23:17:57 +03:00
65794c1b20
add api calls flag 2024-10-14 23:17:17 +03:00
b058f9d770 Merge pull request 'Adapter to enable subdomain of db key prefixes' (#102) from lash/subprefix into master
Reviewed-on: #102
2024-10-14 15:11:07 +02:00
7df77a1343
updated the ValidateAmount to also check the active symbol, updated tests 2024-10-12 20:07:06 +03:00
f5dbfe553d
use the check_balance for the max_amount 2024-10-12 20:06:00 +03:00
7fe8f0b7d5
updated the args under FetchVouchers 2024-10-12 18:03:13 +03:00
a9f9867976
added the TestSetVoucher 2024-10-12 17:36:00 +03:00
b6d24bf929
update the TestCheckBalance 2024-10-12 16:29:12 +03:00
e9684fcf45
check whether the active symbol is empty 2024-10-12 16:28:02 +03:00
a5b1c5b74e
cleaned up the code 2024-10-12 14:32:40 +03:00
672eebb8fb
display the balance based on the symbol 2024-10-11 09:42:08 +03:00
8f834b3d76
ensure that a user is authorized 2024-10-11 09:41:47 +03:00
6c93fb76b1
correctly added the flag and the flag count 2024-10-11 09:36:43 +03:00
ea2df55295
use go-vise v0.2.0 2024-10-10 15:18:13 +03:00
7c08a0f0af
add temp and active balance 2024-10-10 14:48:23 +03:00
9ad7d5a522
set the active symbol 2024-10-09 15:39:06 +03:00
6c904a8b3f
add the temporary and active symbol 2024-10-09 15:38:19 +03:00
9ccb6cc066
use 99 as the quit 2024-10-09 15:05:59 +03:00
2c98a8e133
read token list from json file 2024-10-08 14:34:21 +03:00
4b6fd35e7a
define importable voucher response 2024-10-08 13:43:57 +03:00
b3c7a3a337
Merge remote-tracking branch 'refs/remotes/origin/menu-voucherlist' into menu-voucherlist 2024-10-08 13:43:19 +03:00
7b374ad801
define importable token list 2024-10-08 13:41:09 +03:00
cb4a52e4f2
view a selected voucher and verify the PIN 2024-10-07 16:16:57 +03:00
06ebcc0f07
added swahili template 2024-10-07 16:15:13 +03:00
e4ed9a65bb
store the symbols and balances 2024-10-07 13:49:12 +03:00
755899be4e
store the pre-rendered vouchers list 2024-10-07 08:59:15 +03:00
4dede757d2 Merge branch 'lash/subprefix' into menu-voucherlist 2024-10-07 08:06:37 +03:00
517f980664
get the vouchers list and store in gdbm 2024-10-05 16:56:49 +03:00
31aea6b807
added model and func to fetch vouchers from the API 2024-09-28 14:07:37 +03:00
3a46fda769
use save_temporary_pin and updated tests 2024-09-28 12:26:37 +03:00
c7bdfe90b7 Merge branch 'master' into menu-voucherlist 2024-09-28 11:07:36 +03:00
lash
2a93ea7a0c
Remove out-of-context extend key 2024-09-27 21:16:08 +01:00
lash
f89b1acc6c Merge branch 'master' into lash/subprefix 2024-09-27 21:15:17 +01:00
ddeafe015b Merge pull request 'tests-update' (#100) from tests-update into master
Reviewed-on: #100
2024-09-27 22:14:50 +02:00
lash
1dc8b054eb
Add sub-prefix db wrapper 2024-09-27 21:10:03 +01:00
60134f14e9 Merge branch 'master' into menu-voucherlist 2024-09-27 19:11:35 +03:00
4c33945081
use latest go-vise update 2024-09-27 19:10:08 +03:00
a3ff3be5b1
clean up 2024-09-27 16:32:47 +03:00
ea52a7cf0a
update tests 2024-09-26 23:11:37 +03:00
a993026380
update tests 2024-09-26 23:07:48 +03:00
lash
4ee241714b
Attempt 2 revert changes from ssh 2024-09-26 15:08:15 +01:00
lash
d7dc743fa2 Merge remote-tracking branch 'origin/master' 2024-09-26 15:07:13 +01:00
lash
c220cbb767
Revert accidental merge of ssh 2024-09-26 15:05:17 +01:00
0dc322729f Merge pull request 'SSH server entry point.' (#77) from lash/ssh-2 into master
Reviewed-on: #77
2024-09-26 15:55:51 +02:00
726d6dd338 Merge pull request 'menu-profile-edit' (#82) from menu-profile-edit into master
Reviewed-on: #82
2024-09-26 15:48:55 +02:00
a8b202bd79 Merge branch 'master' into menu-profile-edit 2024-09-25 15:19:04 +02:00
221db4e998
return a numbered list of vouchers 2024-09-25 16:06:06 +03:00
0e376e0d9e
include back and quit 2024-09-25 16:03:08 +03:00
7aa44caea2
add voucher nodes 2024-09-25 15:57:23 +03:00
188cb573dd
add dummy vouchers list 2024-09-25 13:27:13 +03:00
6dbd250694
remove extra spaces 2024-09-25 09:29:26 +03:00
cc760e7698
remove move 2024-09-25 09:20:45 +03:00
8648ea599c
add catch node 2024-09-25 09:20:45 +03:00
77e4c5d43a
reset the positon of LOAD function 2024-09-25 09:20:44 +03:00
b15ba367c4
change position of LOAD function 2024-09-25 09:20:44 +03:00
4db862bc97
LOAD and RELOAD get_amount 2024-09-25 09:20:43 +03:00
6ad67f6adc
change position of LOAD 2024-09-25 09:20:42 +03:00
0caf82a27e Merge pull request 'fix-repeated-nodes' (#81) from fix-repeated-nodes into master
Reviewed-on: #81
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-25 01:48:34 +02:00
a430857309 Merge branch 'master' into fix-repeated-nodes 2024-09-25 01:41:43 +02:00
2643c0c9ff
only have a -d for the engineDebug 2024-09-24 16:11:22 +03:00
b8852e1ab3
add engineDebug and stateDebug flags 2024-09-24 14:10:10 +03:00
0dea34daab
resolve confilict 2024-09-24 09:18:21 +03:00
8057313c78
Merge remote-tracking branch 'remotes/origin/master' into menu-profile-edit 2024-09-24 09:10:17 +03:00
1bd96d0689
setup dedicated nodes for case selection 2024-09-24 09:02:04 +03:00
bbfe46f162
use current rendering symbol for case selection 2024-09-24 09:01:19 +03:00
eff2cbde8b
setup check for unused code 2024-09-24 09:00:31 +03:00
5c85ecffd1
remove unused code 2024-09-24 09:00:13 +03:00
lash
ad1f9233ca Merge branch 'master' into lash/ssh-2 2024-09-23 20:26:47 +01:00
e9fdd1ddbe Merge pull request 'Threaded gdbm' (#76) from lash/thread-gdbm into master
Reviewed-on: #76
2024-09-23 21:25:37 +02:00
8a03b05c90
reset the positon of LOAD function 2024-09-23 21:20:57 +03:00
lash
c18c40b1b8 Merge branch 'lash/thread-gdbm' into lash/ssh-2 2024-09-23 18:56:38 +01:00
5a38b40eaf Merge branch 'lash/thread-gdbm' into fix-repeated-nodes 2024-09-23 20:55:56 +03:00
lash
6112ca175f Merge branch 'master' into lash/thread-gdbm 2024-09-23 18:54:18 +01:00
c7ebca4729
update template text 2024-09-23 20:20:41 +03:00
fa0070b9a8
add swahili templates 2024-09-23 20:20:20 +03:00
4a447fbb45
update profile update 2024-09-23 20:10:37 +03:00
cb32cfe9d8
update dep 2024-09-23 20:09:51 +03:00
cf4e80dcb8
update profile edit nodes 2024-09-23 20:09:37 +03:00
lash
c02d70eaa4
Upgrade dep, improve logging 2024-09-23 17:35:17 +01:00
909d1f3409
change position of LOAD function 2024-09-23 14:16:01 +03:00
66cf90f044
LOAD and RELOAD get_amount 2024-09-23 14:13:26 +03:00
74d5da3987 Merge branch 'lash/thread-gdbm' into fix-repeated-nodes 2024-09-23 11:40:29 +03:00
f215159725
change position of LOAD 2024-09-23 11:38:07 +03:00
a6301163eb
use latest commit from go-vise 0.2.0 2024-09-23 09:59:21 +03:00
lash
a07fce527f Merge branch 'lash/thread-gdbm' into lash/ssh-2 2024-09-22 16:26:59 +01:00
lash
8a5209584d
Update go module deps 2024-09-22 16:25:04 +01:00
lash
ddc8f6dad1
Update module dep 2024-09-22 16:24:09 +01:00
lash
c2b68231f5
Update gitignore 2024-09-22 16:15:40 +01:00
lash
3f3dbf414c
Clarify keyfile use in readme 2024-09-22 16:13:30 +01:00
lash
e4c3e9f015
Add ssh instructions, log host key 2024-09-22 16:09:57 +01:00
lash
9b71244391 Merge branch 'lash/thread-gdbm' into lash/ssh-2 2024-09-22 15:43:18 +01:00
lash
ada1f26b68
Add keystore class, separate keystore tool executable 2024-09-22 15:41:55 +01:00
lash
935b777e57
Factor out ssh service code 2024-09-22 14:41:01 +01:00
lash
7d3ff690f0
Remove useless 'registered' member in thread gdbm 2024-09-22 03:10:58 +01:00
lash
a455cfb854
Return if duplicate connect 2024-09-22 03:09:53 +01:00
lash
19372c17f4
Reinstate ssh executable 2024-09-22 03:06:24 +01:00
lash
1cc2f58ab3
Remove ssh 2024-09-22 03:03:03 +01:00
lash
3756bdc98a Merge remote-tracking branch 'origin/lash/ssh' into lash/ssh 2024-09-22 02:58:18 +01:00
lash
e07f88b368
Settable ssh keyfile, host, port 2024-09-22 02:57:10 +01:00
472624a831 Merge branch 'master' into lash/ssh 2024-09-22 03:38:53 +02:00
lash
84422684c5
Rehabilitate all executables 2024-09-22 02:33:11 +01:00
lash
28fc7a0462
Implement threaded gdbm in storageservice 2024-09-22 02:28:00 +01:00
lash
78d349ccc7
Add threaded gdbm wrapper 2024-09-22 02:21:56 +01:00
lash
0813a619b4
Add hacky db closer function in ssh 2024-09-21 21:32:02 +01:00
lash
5ed9d2643b
WIP can execute single session, but persister fails in next 2024-09-21 18:56:20 +01:00
lash
8dcba67fe9
WIP can execute single session, but persister fails in next 2024-09-21 18:49:11 +01:00
lash
5cac17676a
WIP adapt ssh server sketch to urdt setup 2024-09-21 00:32:39 +01:00
4439cc249a Merge pull request 'go-vise-0.2.0' (#71) from go-vise-0.2.0 into master
Reviewed-on: #71
2024-09-21 00:06:07 +02:00
e754773bce
remove need for initial Exec before loop 2024-09-20 22:16:43 +03:00
63c7c65a00
rename all WriteResult to Flush 2024-09-20 22:15:24 +03:00
9925b3a2e5
use updated latest commit from go-vise 0.2.0 2024-09-20 22:12:52 +03:00
a1056cf287
removed unused code 2024-09-20 13:54:23 +03:00
e6e701a609
remove init and use Exec 2024-09-20 13:13:20 +03:00
b66891a646
use latest commit from go-vise 0.2.0 2024-09-20 13:10:44 +03:00
7197382911 Merge pull request 'Code refactor' (#66) from wip-main-refactor into master
Reviewed-on: #66
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-19 20:19:16 +02:00
5c5f4cbc8e Merge branch 'master' into wip-main-refactor 2024-09-19 20:19:06 +02:00
e581a1ad2f
factor out getflag 2024-09-19 21:04:09 +03:00
929e812992 Merge pull request 'at-return-output' (#63) from at-return-output into master
Reviewed-on: #63
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-19 18:23:13 +02:00
3fe66466c5
renamed test file and added server.go tests 2024-09-19 18:52:13 +03:00
f6979868e5
added at_session_handler tests 2024-09-19 17:19:09 +03:00
e8be64ae28
added http related mocks 2024-09-19 17:18:14 +03:00
e4d8bfad7b
moved the mocks package 2024-09-19 17:17:28 +03:00
96358959b4
remove unused code 2024-09-19 17:04:08 +03:00
bc9dfe4f65
remove redundant code 2024-09-19 17:01:23 +03:00
ffa00ae15c
refactor code 2024-09-19 16:18:21 +03:00
13294b42d3
refactor code 2024-09-19 16:16:17 +03:00
81159e77d1
refactor code 2024-09-19 16:15:29 +03:00
5bd51b280e
add storage service for menu 2024-09-19 15:57:11 +03:00
71e1ae6e3c
add function registration service 2024-09-19 15:54:23 +03:00
10de039da0
remove obsolete code 2024-09-18 15:42:56 +03:00
27aa71e0ee
only set the code to 200 if no error exists 2024-09-18 15:11:49 +03:00
a9a429824c
return if the sessionId cannot be retrieved from the request 2024-09-18 14:52:56 +03:00
4098ac8a19 Merge pull request 'change-language' (#58) from change-language into master
Reviewed-on: #58
2024-09-17 18:31:10 +02:00
2982f08b41 Merge branch 'master' into change-language 2024-09-17 18:30:52 +02:00
75ab9c66a3
run TestSetLanguage with execPath instead of input 2024-09-17 18:54:07 +03:00
95f02231b3 Merge pull request 'Unit tests' (#62) from wip-unit-tests into master
Reviewed-on: #62
2024-09-17 15:55:33 +02:00
599f7a2857
add more tests 2024-09-17 15:44:22 +03:00
2e7c07e6f4
added missing functions to the other getHandlers 2024-09-17 15:31:44 +03:00
01f7571185
use dedicated nodes to set the language 2024-09-17 15:29:21 +03:00
065c8e5c1d
use a single set_language function for setting and changing language 2024-09-17 15:26:50 +03:00
dc14480519 remove extra space 2024-09-16 17:29:27 +03:00
aaf4923f64 ensure a PIN is set before changing the language 2024-09-16 17:29:27 +03:00
e9c645bd87 added the profile info on the swa menu template 2024-09-16 17:29:27 +03:00
1be6da9139 remove new lines on templates and menus to clean up the empty spaces on the menu 2024-09-16 17:29:27 +03:00
fa2930d93a added quit menu to handle eng and swa 2024-09-16 17:29:27 +03:00
a409e292ab add the change_language functionality to the account menu 2024-09-16 17:29:27 +03:00
00c86a2850 change language 2024-09-16 17:29:27 +03:00
4daac7e90b added change_language node and templates 2024-09-16 17:29:27 +03:00
06e23565df added the SetNewLanguage function 2024-09-16 17:29:27 +03:00
b19188165b Merge pull request 'wip-menu-help' (#56) from wip-menu-help into master
Reviewed-on: #56
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-16 16:21:49 +02:00
2ed9f083bb add quit with helpline info
add quit with helpline info
2024-09-16 15:17:56 +01:00
e323ffa078 Merge pull request 'Pin reset' (#59) from wip-pin-reset into master
Reviewed-on: #59
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-09-16 16:14:52 +02:00
68de83af97 Merge pull request 'Allow setting go-vise path for asm executable in makefile' (#57) from lash/vise-make-var into master
Reviewed-on: #57
2024-09-16 15:49:58 +02:00
lash
8ae3372c36
Allow setting go-vise path for asm executable in makefile 2024-09-14 21:34:11 +01:00
210 changed files with 5935 additions and 1889 deletions

21
.env.example Normal file
View File

@ -0,0 +1,21 @@
#Serve Http
PORT=7123
HOST=127.0.0.1
#PostgreSQL
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=strongpass
DB_NAME=urdt_ussd
DB_PORT=5432
DB_SSLMODE=disable
DB_TIMEZONE=Africa/Nairobi
#External API Calls
CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create
TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/
BALANCE_URL=https://custodial.sarafu.africa/api/account/status/
TRACK_URL=http://localhost:5003/api/v2/account/status
#AfricasTalking USSD POST endpoint
AT_ENDPOINT=/ussd/africastalking

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ go.work*
**/*/*.bin **/*/*.bin
**/*/.state/ **/*/.state/
cmd/.state/ cmd/.state/
id_*
*.gdbm
*.log

View File

@ -1,8 +1,91 @@
# ussd # URDT USSD service
> USSD This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine.
USSD service. ## Prerequisites
### 1. [go-vise](https://github.com/nolash/go-vise)
Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git`
```
git clone https://git.defalsify.org/vise.git
```
## Setup
1. Clone the ussd repo in its own directory
```
git clone https://git.grassecon.net/urdt/ussd.git
```
2. Navigate to the project directory.
3. Enter the `services/registration` subfolder:
```
cd services/registration
```
4. make the .bin files from the .vis files
```
make VISE_PATH=/var/path/to/your/go-vise -B
```
5. Return to the project root (`cd ../..`)
6. Run the USSD menu
```
go run cmd/main.go -session-id=0712345678
```
## Running the different binaries
1. ### CLI:
```
go run cmd/main.go -session-id=0712345678
```
2. ### Africastalking:
```
go run cmd/africastalking/main.go
```
3. ### Async:
```
go run cmd/async/main.go
```
4. ### Http:
```
go run cmd/http/main.go
```
## Flags
Below are the supported flags:
1. `-session-id`:
Specifies the session ID. (CLI only).
Default: `075xx2123`.
Example:
```
go run cmd/main.go -session-id=0712345678
```
2. `-d`:
Enables engine debug output.
Default: `false`.
Example:
```
go run cmd/main.go -session-id=0712345678 -d
```
3. `-db`:
Specifies the database type.
Default: `gdbm`.
Example:
```
go run cmd/main.go -session-id=0712345678 -d -db=postgres
```
>Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file.
## License ## License

View File

@ -1,9 +1,13 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -12,32 +16,72 @@ import (
"strings" "strings"
"syscall" "syscall"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"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/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
httpserver "git.grassecon.net/urdt/ussd/internal/http" httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
) )
type atRequestParser struct {} func init() {
initializers.LoadEnvVariables()
func(arp *atRequestParser) GetSessionId(rq any) (string, error) { logFile := "urdt-ussd-africastalking.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
}
type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
ErrorLogger.Println("got an invalid request:", rq)
return "", handlers.ErrInvalidRequest return "", handlers.ErrInvalidRequest
} }
// Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body)
if err != nil {
ErrorLogger.Println("failed to read request body:", 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 {
ErrorLogger.Println("failed to marshal request body:", err)
} else {
InfoLogger.Println("Received request:", string(logBytes))
}
if err := rqv.ParseForm(); err != nil { if err := rqv.ParseForm(); err != nil {
ErrorLogger.Println("failed to parse form data: %v", err)
return "", fmt.Errorf("failed to parse form data: %v", err) return "", fmt.Errorf("failed to parse form data: %v", err)
} }
@ -49,7 +93,7 @@ func(arp *atRequestParser) GetSessionId(rq any) (string, error) {
return phoneNumber, nil return phoneNumber, nil
} }
func(arp *atRequestParser) GetInput(rq any) ([]byte, error) { func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
return nil, handlers.ErrInvalidRequest return nil, handlers.ErrInvalidRequest
@ -68,144 +112,55 @@ func(arp *atRequestParser) GetInput(rq any) ([]byte, error) {
return []byte(parts[len(parts)-1]), nil return []byte(parts[len(parts)-1]), nil
} }
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
flagParser := asm.NewFlagParser().WithDebug()
_, err := flagParser.Load(fp)
if err != nil {
return nil, err
}
return flagParser, nil
}
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
if err != nil {
return nil, err
}
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rs.AddLocalFunc("quit", ussdHandlers.Quit)
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
return ussdHandlers, nil
}
func ensureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
return store, nil
}
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func main() { func main() {
config.LoadConfig()
var dbDir string var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string
var engineDebug bool var engineDebug bool
var stateDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", 7123, "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
flagParser, err := getFlags(pfp, true)
if err != nil {
os.Exit(1)
}
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(16), FlagCount: uint32(128),
}
if stateDebug {
cfg.StateDebug = true
} }
if engineDebug { if engineDebug {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
rs, err := getResource(resourceDir, ctx) menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = ensureDbDir(dbDir) err = menuStorageService.EnsureDbDir()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
userdataStore := getUserdataDb(dbDir, ctx) userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -217,13 +172,22 @@ func main() {
os.Exit(1) os.Exit(1)
} }
hl, err := getHandler(flagParser, dbResource, userdataStore) lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
stateStore, err := getStateStore(dbDir, ctx) accountService := remote.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
stateStore, err := menuStorageService.GetStateStore(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -233,9 +197,13 @@ func main() {
rp := &atRequestParser{} rp := &atRequestParser{}
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)
mux := http.NewServeMux()
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
s := &http.Server{ s := &http.Server{
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
Handler: sh, Handler: mux,
} }
s.RegisterOnShutdown(sh.Shutdown) s.RegisterOnShutdown(sh.Shutdown)

View File

@ -4,180 +4,111 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log"
"os" "os"
"os/signal" "os/signal"
"path" "path"
"syscall" "syscall"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
) )
func init() {
initializers.LoadEnvVariables()
logFile := "urdt-ussd-async.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
}
type asyncRequestParser struct { type asyncRequestParser struct {
sessionId string sessionId string
input []byte input []byte
} }
func(p *asyncRequestParser) GetSessionId(r any) (string, error) { func (p *asyncRequestParser) GetSessionId(r any) (string, error) {
return p.sessionId, nil return p.sessionId, nil
} }
func(p *asyncRequestParser) GetInput(r any) ([]byte, error) { func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
return p.input, nil return p.input, nil
} }
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
flagParser := asm.NewFlagParser().WithDebug()
_, err := flagParser.Load(fp)
if err != nil {
return nil, err
}
return flagParser, nil
}
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
if err != nil {
return nil, err
}
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rs.AddLocalFunc("quit", ussdHandlers.Quit)
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
return ussdHandlers, nil
}
func ensureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
return store, nil
}
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func main() { func main() {
config.LoadConfig()
var sessionId string var sessionId string
var dbDir string var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string
var engineDebug bool var engineDebug bool
var stateDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", 7123, "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
flagParser, err := getFlags(pfp, true)
if err != nil {
os.Exit(1)
}
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(16), FlagCount: uint32(128),
}
if stateDebug {
cfg.StateDebug = true
} }
if engineDebug { if engineDebug {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
rs, err := getResource(resourceDir, ctx) menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = ensureDbDir(dbDir) err = menuStorageService.EnsureDbDir()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
userdataStore := getUserdataDb(dbDir, ctx) userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -189,13 +120,17 @@ func main() {
os.Exit(1) os.Exit(1)
} }
hl, err := getHandler(flagParser, dbResource, userdataStore) lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
accountService := remote.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
stateStore, err := getStateStore(dbDir, ctx) stateStore, err := menuStorageService.GetStateStore(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -228,22 +163,26 @@ func main() {
for true { for true {
rqs, err = sh.Process(rqs) rqs, err = sh.Process(rqs)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
fmt.Errorf("error in process: %v", err) fmt.Errorf("error in process: %v", err)
os.Exit(1) os.Exit(1)
} }
rqs, err = sh.Output(rqs) rqs, err = sh.Output(rqs)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
fmt.Errorf("error in output: %v", err) fmt.Errorf("error in output: %v", err)
os.Exit(1) os.Exit(1)
} }
rqs, err = sh.Reset(rqs) rqs, err = sh.Reset(rqs)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
fmt.Errorf("error in reset: %v", err) fmt.Errorf("error in reset: %v", err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("") fmt.Println("")
_, err = fmt.Scanln(&rqs.Input) _, err = fmt.Scanln(&rqs.Input)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "error in input", "err", err)
fmt.Errorf("error in input: %v", err) fmt.Errorf("error in input: %v", err)
os.Exit(1) os.Exit(1)
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -11,161 +12,92 @@ import (
"strconv" "strconv"
"syscall" "syscall"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd" "git.grassecon.net/urdt/ussd/config"
"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" httpserver "git.grassecon.net/urdt/ussd/internal/http"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
) )
func getFlags(fp string, debug bool) (*asm.FlagParser, error) { func init() {
flagParser := asm.NewFlagParser().WithDebug() initializers.LoadEnvVariables()
_, err := flagParser.Load(fp)
logFile := "urdt-ussd-http.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil { if err != nil {
return nil, err log.Fatal(err)
} }
return flagParser, nil
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
} }
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
if err != nil {
return nil, err
}
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rs.AddLocalFunc("quit", ussdHandlers.Quit)
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
return ussdHandlers, nil
}
func ensureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
return store, nil
}
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func main() { func main() {
config.LoadConfig()
var dbDir string var dbDir string
var resourceDir string var resourceDir string
var size uint var size uint
var database string
var engineDebug bool var engineDebug bool
var stateDebug bool
var host string var host string
var port uint var port uint
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.StringVar(&host, "h", "127.0.0.1", "http host") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
flag.UintVar(&port, "p", 7123, "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
flagParser, err := getFlags(pfp, true)
if err != nil {
os.Exit(1)
}
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(16), FlagCount: uint32(128),
}
if stateDebug {
cfg.StateDebug = true
} }
if engineDebug { if engineDebug {
cfg.EngineDebug = true cfg.EngineDebug = true
} }
rs, err := getResource(resourceDir, ctx) menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = ensureDbDir(dbDir) err = menuStorageService.EnsureDbDir()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
userdataStore := getUserdataDb(dbDir, ctx) userdataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -177,13 +109,22 @@ func main() {
os.Exit(1) os.Exit(1)
} }
hl, err := getHandler(flagParser, dbResource, userdataStore) lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdataStore)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
stateStore, err := getStateStore(dbDir, ctx) accountService := remote.AccountService{}
hl, err := lhs.GetHandler(&accountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
stateStore, err := menuStorageService.GetStateStore(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)

View File

@ -4,132 +4,57 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log"
"os" "os"
"path" "path"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"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/persist"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/remote"
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
) )
func getParser(fp string, debug bool) (*asm.FlagParser, error) { func init() {
flagParser := asm.NewFlagParser().WithDebug() initializers.LoadEnvVariables()
_, err := flagParser.Load(fp)
logFile := "urdt-ussd-cli.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil { if err != nil {
return nil, err log.Fatal(err)
} }
return flagParser, nil
}
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.Persister, userdataStore db.Db) (*ussd.Handlers, error) { InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) // Inject into remote package
if err != nil { remote.InfoLogger = InfoLogger
return nil, err remote.ErrorLogger = ErrorLogger
}
ussdHandlers = ussdHandlers.WithPersister(pe)
rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
rs.AddLocalFunc("quit", ussdHandlers.Quit)
rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
rs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
rs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
rs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
return ussdHandlers, nil
}
func ensureDbDir(dbDir string) error {
err := os.MkdirAll(dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) {
err := ensureDbDir(dbDir)
if err != nil {
return nil, err
}
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "state.gdbm")
store.Connect(ctx, storeFile)
pr := persist.NewPersister(store)
return pr, nil
}
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
store := gdbmdb.NewGdbmDb()
storeFile := path.Join(dbDir, "userdata.gdbm")
store.Connect(ctx, storeFile)
return store
}
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
store := fsdb.NewFsDb()
err := store.Connect(ctx, resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
return rfs, nil
}
func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine {
en := engine.NewEngine(cfg, rs)
en = en.WithPersister(pr)
return en
} }
func main() { func main() {
config.LoadConfig()
var dbDir string var dbDir string
var size uint var size uint
var sessionId string var sessionId string
var debug bool var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&debug, "d", false, "use engine debug output") flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.UintVar(&size, "s", 160, "max size of output") flag.UintVar(&size, "s", 160, "max size of output")
flag.Parse() flag.Parse()
@ -137,33 +62,38 @@ func main() {
ctx := context.Background() ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
flagParser, err := getParser(pfp, true)
if err != nil {
os.Exit(1)
}
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
SessionId: sessionId, SessionId: sessionId,
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(16), FlagCount: uint32(128),
} }
rs, err := getResource(scriptDir, ctx) resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
pe, err := getPersister(dbDir, ctx) rs, err := menuStorageService.GetResource(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
store := getUserdataDb(dbDir, ctx) pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userdatastore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
@ -175,25 +105,29 @@ func main() {
os.Exit(1) os.Exit(1)
} }
hl, err := getHandler(flagParser, dbResource, pe, store) lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userdatastore)
lhs.SetPersister(pe)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
en := getEngine(cfg, rs, pe) accountService := remote.AccountService{}
en = en.WithFirst(hl.Init) hl, err := lhs.GetHandler(&accountService)
if debug {
en = en.WithDebug(nil)
}
_, err = en.Init(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err) fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1) os.Exit(1)
} }
err = engine.Loop(ctx, en, os.Stdin, os.Stdout) en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
if engineDebug {
en = en.WithDebug(nil)
}
err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
os.Exit(1) os.Exit(1)

View File

@ -1,7 +1,9 @@
package utils package common
import ( import (
"encoding/binary" "encoding/binary"
"git.defalsify.org/vise.git/logging"
) )
type DataTyp uint16 type DataTyp uint16
@ -22,7 +24,18 @@ const (
DATA_OFFERINGS DATA_OFFERINGS
DATA_RECIPIENT DATA_RECIPIENT
DATA_AMOUNT DATA_AMOUNT
DATA_TEMPORARY_PIN DATA_TEMPORARY_VALUE
DATA_ACTIVE_SYM
DATA_ACTIVE_BAL
DATA_BLOCKED_NUMBER
DATA_PUBLIC_KEY_REVERSE
DATA_ACTIVE_DECIMAL
DATA_ACTIVE_ADDRESS
DATA_TRANSACTIONS
)
var (
logg = logging.NewVanilla().WithDomain("urdt-common")
) )
func typToBytes(typ DataTyp) []byte { func typToBytes(typ DataTyp) []byte {

31
common/hex.go Normal file
View File

@ -0,0 +1,31 @@
package common
import (
"encoding/hex"
"strings"
)
func NormalizeHex(s string) (string, error) {
if len(s) >= 2 {
if s[:2] == "0x" {
s = s[2:]
}
}
r, err := hex.DecodeString(s)
if err != nil {
return "", err
}
return hex.EncodeToString(r), nil
}
func IsSameHex(left string, right string) bool {
bl, err := NormalizeHex(left)
if err != nil {
return false
}
br, err := NormalizeHex(left)
if err != nil {
return false
}
return strings.Compare(bl, br) == 0
}

52
common/storage.go Normal file
View File

@ -0,0 +1,52 @@
package common
import (
"context"
"errors"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/persist"
"git.grassecon.net/urdt/ussd/internal/storage"
)
func StoreToDb(store *UserDataStore) db.Db {
return store.Db
}
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
return storage.NewSubPrefixDb(store.Db, pfx)
}
type StorageServices interface {
GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) (db.Db, error)
GetResource(ctx context.Context) (resource.Resource, error)
EnsureDbDir() error
}
type StorageService struct {
svc *storage.MenuStorageService
}
func NewStorageService(dbDir string) *StorageService {
return &StorageService{
svc: storage.NewMenuStorageService(dbDir, ""),
}
}
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
return ss.svc.GetPersister(ctx)
}
func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
return ss.svc.GetUserdataDb(ctx)
}
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
return nil, errors.New("not implemented")
}
func(ss *StorageService) EnsureDbDir() error {
return ss.svc.EnsureDbDir()
}

View File

@ -1,4 +1,4 @@
package utils package common
import ( import (
"context" "context"
@ -16,7 +16,7 @@ type UserDataStore struct {
db.Db db.Db
} }
// ReadEntry retrieves an entry from the store based on the provided parameters. // ReadEntry retrieves an entry to the userdata store.
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId) store.SetSession(sessionId)
@ -24,6 +24,8 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
return store.Get(ctx, k) return store.Get(ctx, k)
} }
// WriteEntry adds an entry to the userdata store.
// BUG: this uses sessionId twice
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId) store.SetSession(sessionId)

157
common/vouchers.go Normal file
View File

@ -0,0 +1,157 @@
package common
import (
"context"
"fmt"
"strings"
"git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// VoucherMetadata helps organize data fields
type VoucherMetadata struct {
Symbols string
Balances string
Decimals string
Addresses string
}
// ProcessVouchers converts holdings into formatted strings
func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
var data VoucherMetadata
var symbols, balances, decimals, addresses []string
for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance))
decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
}
data.Symbols = strings.Join(symbols, "\n")
data.Balances = strings.Join(balances, "\n")
data.Decimals = strings.Join(decimals, "\n")
data.Addresses = strings.Join(addresses, "\n")
return data
}
//func StoreVouchers(db storage.PrefixDb, data VoucherMetadata) {
// value, err := db.Put(ctx, []byte(key))
// if err != nil {
// return nil, fmt.Errorf("failed to get %s: %v", key, err)
// }
// data[key] = string(value)
// }
//}
// GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []string{"sym", "bal", "deci", "addr"}
data := make(map[string]string)
for _, key := range keys {
value, err := db.Get(ctx, []byte(key))
if err != nil {
return nil, fmt.Errorf("failed to get %s: %v", key, err)
}
data[key] = string(value)
}
symbol, balance, decimal, address := MatchVoucher(input,
data["sym"],
data["bal"],
data["deci"],
data["addr"])
if symbol == "" {
return nil, nil
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: string(symbol),
Balance: string(balance),
TokenDecimals: string(decimal),
ContractAddress: string(address),
}, nil
}
// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) {
symList := strings.Split(symbols, "\n")
balList := strings.Split(balances, "\n")
decList := strings.Split(decimals, "\n")
addrList := strings.Split(addresses, "\n")
logg.Tracef("found" , "symlist", symList, "syms", symbols, "input", input)
for i, sym := range symList {
parts := strings.SplitN(sym, ":", 2)
if input == parts[0] || strings.EqualFold(input, parts[1]) {
symbol = parts[1]
if i < len(balList) {
balance = strings.SplitN(balList[i], ":", 2)[1]
}
if i < len(decList) {
decimal = strings.SplitN(decList[i], ":", 2)[1]
}
if i < len(addrList) {
address = strings.SplitN(addrList[i], ":", 2)[1]
}
break
}
}
return
}
// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress)
if err := store.WriteEntry(ctx, sessionId, DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil {
return err
}
return nil
}
// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
temp_data, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
if err != nil {
return nil, err
}
values := strings.SplitN(string(temp_data), ",", 4)
data := &dataserviceapi.TokenHoldings{}
data.TokenSymbol = values[0]
data.Balance = values[1]
data.TokenDecimals = values[2]
data.ContractAddress = values[3]
return data, nil
}
// UpdateVoucherData sets the active voucher data and clears the temporary voucher data in the DataStore.
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.TraceCtxf(ctx, "dtal", "data", data)
// Active voucher data entries
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(data.TokenSymbol),
DATA_ACTIVE_BAL: []byte(data.Balance),
DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress),
}
// Write active data
for key, value := range activeEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}

197
common/vouchers_test.go Normal file
View File

@ -0,0 +1,197 @@
package common
import (
"context"
"fmt"
"testing"
"github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require"
"git.grassecon.net/urdt/ussd/internal/storage"
memdb "git.defalsify.org/vise.git/db/mem"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// InitializeTestDb sets up and returns an in-memory database and store.
func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) {
ctx := context.Background()
// Initialize memDb
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
require.NoError(t, err, "Failed to connect to memDb")
// Create UserDataStore with memDb
store := &UserDataStore{Db: db}
t.Cleanup(func() {
db.Close() // Ensure the DB is closed after each test
})
return ctx, store
}
func TestMatchVoucher(t *testing.T) {
symbols := "1:SRF\n2:MILO"
balances := "1:100\n2:200"
decimals := "1:6\n2:4"
addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa"
// Test for valid voucher
symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses)
// Assertions for valid voucher
assert.Equal(t, "MILO", symbol)
assert.Equal(t, "200", balance)
assert.Equal(t, "4", decimal)
assert.Equal(t, "0x41c188d63Qa", address)
// Test for non-existent voucher
symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses)
// Assertions for non-match
assert.Equal(t, "", symbol)
assert.Equal(t, "", balance)
assert.Equal(t, "", decimal)
assert.Equal(t, "", address)
}
func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"},
}
expectedResult := VoucherMetadata{
Symbols: "1:SRF\n2:MILO",
Balances: "1:100\n2:200",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}
result := ProcessVouchers(holdings)
assert.Equal(t, expectedResult, result)
}
func TestGetVoucherData(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
// Test voucher data
mockData := map[string][]byte{
"sym": []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
}
// Put the data
for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value))
if err != nil {
t.Fatal(err)
}
}
result, err := GetVoucherData(ctx, spdb, "1")
assert.NoError(t, err)
assert.Equal(t, "SRF", result.TokenSymbol)
assert.Equal(t, "100", result.Balance)
assert.Equal(t, "6", result.TokenDecimals)
assert.Equal(t, "0xd4c288865Ce", result.ContractAddress)
}
func TestStoreTemporaryVoucher(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test data
voucherData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Execute the function being tested
err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData)
require.NoError(t, err)
// Verify stored data
expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9")
storedValue, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
require.NoError(t, err)
require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", DATA_TEMPORARY_VALUE)
}
func TestGetTemporaryVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test voucher data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Store the data
err := StoreTemporaryVoucher(ctx, store, sessionId, tempData)
require.NoError(t, err)
// Execute the function being tested
data, err := GetTemporaryVoucherData(ctx, store, sessionId)
require.NoError(t, err)
require.Equal(t, tempData, data)
}
func TestUpdateVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// New voucher data
newData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Old temporary data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "OLD",
Balance: "100",
TokenDecimals: "8",
ContractAddress: "0xold",
}
require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData))
// Execute update
err := UpdateVoucherData(ctx, store, sessionId, newData)
require.NoError(t, err)
// Verify active data was stored correctly
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(newData.TokenSymbol),
DATA_ACTIVE_BAL: []byte(newData.Balance),
DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress),
}
for key, expectedValue := range activeEntries {
storedValue, err := store.ReadEntry(ctx, sessionId, key)
require.NoError(t, err)
require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key)
}
}

View File

@ -1,10 +1,70 @@
package config package config
import (
"net/url"
"git.grassecon.net/urdt/ussd/initializers"
const (
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create"
TrackStatusURL = "https://custodial.sarafu.africa/api/track/"
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
) )
const (
createAccountPath = "/api/v2/account/create"
trackStatusPath = "/api/track"
balancePathPrefix = "/api/account"
trackPath = "/api/v2/account/status"
voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10"
voucherDataPathPrefix = "/api/v1/token"
)
var (
custodialURLBase string
dataURLBase string
CustodialAPIKey string
DataAPIKey string
)
var (
CreateAccountURL string
TrackStatusURL string
BalanceURL string
TrackURL string
VoucherHoldingsURL string
VoucherTransfersURL string
VoucherDataURL string
)
func setBase() error {
var err error
custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003")
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
CustodialAPIKey = initializers.GetEnv("CUSTODIAL_API_KEY", "xd")
DataAPIKey = initializers.GetEnv("DATA_API_KEY", "xd")
_, err = url.JoinPath(custodialURLBase, "/foo")
if err != nil {
return err
}
_, err = url.JoinPath(dataURLBase, "/bar")
if err != nil {
return err
}
return nil
}
// LoadConfig initializes the configuration values after environment variables are loaded.
func LoadConfig() error {
err := setBase()
if err != nil {
return err
}
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
TrackURL, _ = url.JoinPath(custodialURLBase, trackPath)
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
return nil
}

View File

@ -0,0 +1,7 @@
{
"admins": [
{
"phonenumber" : "<replace with any admin number to test with >"
}
]
}

View File

@ -0,0 +1,47 @@
package commands
import (
"context"
"encoding/json"
"os"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/internal/utils"
)
var (
logg = logging.NewVanilla().WithDomain("adminstore")
)
type Admin struct {
PhoneNumber string `json:"phonenumber"`
}
type Config struct {
Admins []Admin `json:"admins"`
}
func Seed(ctx context.Context) error {
var config Config
adminstore, err := utils.NewAdminStore(ctx, "../admin_numbers")
store := adminstore.FsStore
if err != nil {
return err
}
defer store.Close()
data, err := os.ReadFile("admin_numbers.json")
if err != nil {
return err
}
if err := json.Unmarshal(data, &config); err != nil {
return err
}
for _, admin := range config.Admins {
err := store.Put(ctx, []byte(admin.PhoneNumber), []byte("1"))
if err != nil {
logg.Printf(logging.LVL_DEBUG, "Failed to insert admin number", admin.PhoneNumber)
return err
}
}
return nil
}

17
devtools/admin/main.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"context"
"log"
"git.grassecon.net/urdt/ussd/devtools/admin/commands"
)
func main() {
ctx := context.Background()
err := commands.Seed(ctx)
if err != nil {
log.Fatalf("Failed to initialize a list of admins with error %s", err)
}
}

27
go.mod
View File

@ -1,24 +1,43 @@
module git.grassecon.net/urdt/ussd module git.grassecon.net/urdt/ussd
go 1.22.6 go 1.23.0
toolchain go1.23.2
require ( require (
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
github.com/peteole/testdata-loader v0.3.0 github.com/peteole/testdata-loader v0.3.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1
) )
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
require ( require (
github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect

50
go.sum
View File

@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f h1:CuJvG3NyMoRtHUim4aZdrfjjJBg2AId7z0yp7Q97bRM= git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/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=
@ -8,29 +8,67 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s=
github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A=
github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I=
github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

34
initializers/load.go Normal file
View File

@ -0,0 +1,34 @@
package initializers
import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
func LoadEnvVariables() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
// Helper to get environment variables with a default fallback
func GetEnv(key, defaultVal string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultVal
}
// Helper to safely convert environment variables to uint
func GetEnvUint(key string, defaultVal uint) uint {
if value, exists := os.LookupEnv(key); exists {
if parsed, err := strconv.Atoi(value); err == nil && parsed >= 0 {
return uint(parsed)
}
}
return defaultVal
}

View File

@ -46,7 +46,7 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
var err error var err error
var ok bool var ok bool
logg.InfoCtxf(rqs.Ctx, "new request", rqs) logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs)
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId) rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
if err != nil { if err != nil {
@ -71,19 +71,7 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
} }
rqs.Engine = en rqs.Engine = en
r, err = rqs.Engine.Init(rqs.Ctx)
if err != nil {
perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
rqs.Storage = nil
if perr != nil {
logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
}
return rqs, err
}
if r && len(rqs.Input) > 0 {
r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input) r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
}
if err != nil { if err != nil {
perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage) perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
rqs.Storage = nil rqs.Storage = nil
@ -99,7 +87,7 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) { func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) {
var err error var err error
_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer) _, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
return rqs, err return rqs, err
} }

View File

@ -0,0 +1,127 @@
package handlers
import (
"context"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
"git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/remote"
)
type HandlerService interface {
GetHandler() (*ussd.Handlers, error)
}
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
flagParser := asm.NewFlagParser().WithDebug()
_, err := flagParser.Load(fp)
if err != nil {
return nil, err
}
return flagParser, nil
}
type LocalHandlerService struct {
Parser *asm.FlagParser
DbRs *resource.DbResource
Pe *persist.Persister
UserdataStore *db.Db
AdminStore *utils.AdminStore
Cfg engine.Config
Rs resource.Resource
}
func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) {
parser, err := getParser(fp, debug)
if err != nil {
return nil, err
}
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
if err != nil {
return nil, err
}
return &LocalHandlerService{
Parser: parser,
DbRs: dbResource,
AdminStore: adminstore,
Cfg: cfg,
Rs: rs,
}, nil
}
func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) {
ls.Pe = Pe
}
func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
ls.UserdataStore = db
}
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
if err != nil {
return nil, err
}
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit)
ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
return ussdHandlers, nil
}
// TODO: enable setting of sessionId on engine init time
func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine {
en := engine.NewEngine(ls.Cfg, ls.Rs)
en = en.WithPersister(ls.Pe)
return en
}

View File

@ -1,112 +0,0 @@
package server
import (
"encoding/json"
"io"
"net/http"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/models"
)
type AccountServiceInterface interface {
CheckBalance(publicKey string) (string, error)
CreateAccount() (*models.AccountResponse, error)
CheckAccountStatus(trackingId string) (string, error)
}
type AccountService struct {
}
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
//
// Parameters:
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
// AccountResponse struct can be used here to check the account status during a transaction.
//
//
// Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
//
func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) {
resp, err := http.Get(config.TrackStatusURL + trackingId)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp)
if err != nil {
return "", err
}
status := trackResp.Result.Transaction.Status
return status, nil
}
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(publicKey string) (string, error) {
resp, err := http.Get(config.BalanceURL + publicKey)
if err != nil {
return "0.0", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "0.0", err
}
var balanceResp models.BalanceResponse
err = json.Unmarshal(body, &balanceResp)
if err != nil {
return "0.0", err
}
balance := balanceResp.Result.Balance
return balance, nil
}
//CreateAccount creates a new account in the custodial system.
// Returns:
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
// If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var accountResp models.AccountResponse
err = json.Unmarshal(body, &accountResp)
if err != nil {
return nil, err
}
return &accountResp, nil
}

View File

@ -37,8 +37,6 @@ type RequestSession struct {
Continue bool Continue bool
} }
type engineMaker func(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine
// TODO: seems like can remove this. // TODO: seems like can remove this.
type RequestParser interface { type RequestParser interface {
GetSessionId(rq any) (string, error) GetSessionId(rq any) (string, error)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +0,0 @@
package mocks
import (
"context"
"git.defalsify.org/vise.git/lang"
"github.com/stretchr/testify/mock"
)
type MockDb struct {
mock.Mock
}
func (m *MockDb) SetPrefix(prefix uint8) {
m.Called(prefix)
}
func (m *MockDb) Prefix() uint8 {
args := m.Called()
return args.Get(0).(uint8)
}
func (m *MockDb) Safe() bool {
args := m.Called()
return args.Get(0).(bool)
}
func (m *MockDb) SetLanguage(language *lang.Language) {
m.Called(language)
}
func (m *MockDb) SetLock(uint8, bool) error {
args := m.Called()
return args.Error(0)
}
func (m *MockDb) Connect(ctx context.Context, connectionStr string) error {
args := m.Called(ctx, connectionStr)
return args.Error(0)
}
func (m *MockDb) SetSession(sessionId string) {
m.Called(sessionId)
}
func (m *MockDb) Put(ctx context.Context, key, value []byte) error {
args := m.Called(ctx, key, value)
return args.Error(0)
}
func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) {
args := m.Called(ctx, key)
return nil, args.Error(0)
}
func (m *MockDb) Close() error {
args := m.Called(nil)
return args.Error(0)
}

View File

@ -1,26 +0,0 @@
package mocks
import (
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/stretchr/testify/mock"
)
// MockAccountService implements AccountServiceInterface for testing
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
args := m.Called()
return args.Get(0).(*models.AccountResponse), args.Error(1)
}
func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
args := m.Called(publicKey)
return args.String(0), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
args := m.Called(trackingId)
return args.String(0), args.Error(1)
}

View File

@ -1,24 +0,0 @@
package mocks
import (
"context"
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/internal/utils"
"github.com/stretchr/testify/mock"
)
type MockUserDataStore struct {
db.Db
mock.Mock
}
func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) {
args := m.Called(ctx, sessionId, typ)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error {
args := m.Called(ctx, sessionId, typ, value)
return args.Error(0)
}

View File

@ -32,6 +32,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
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
} }
rqs.Config = cfg rqs.Config = cfg
rqs.Input, err = rp.GetInput(req) rqs.Input, err = rp.GetInput(req)
@ -43,14 +44,12 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
rqs, err = ash.Process(rqs) rqs, err = ash.Process(rqs)
switch err { switch err {
case handlers.ErrStorage: case nil: // set code to 200 if no err
code = 500 code = 200
case handlers.ErrEngineInit: case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType:
code = 500
case handlers.ErrEngineExec:
code = 500 code = 500
default: default:
code = 200 code = 500
} }
if code != 200 { if code != 200 {
@ -88,6 +87,6 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque
return rqs, err return rqs, err
} }
_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer) _, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
return rqs, err return rqs, err
} }

449
internal/http/http_test.go Normal file
View File

@ -0,0 +1,449 @@
package http
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"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 TestNewATSessionHandler(t *testing.T) {
mockHandler := &httpmocks.MockRequestHandler{}
ash := NewATSessionHandler(mockHandler)
if ash == nil {
t.Fatal("NewATSessionHandler returned nil")
}
if ash.SessionHandler == nil {
t.Fatal("SessionHandler is nil")
}
}
func TestATSessionHandler_ServeHTTP(t *testing.T) {
tests := []struct {
name string
setupMocks func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine)
formData url.Values
expectedStatus int
expectedBody string
}{
{
name: "Successful request",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
req := rq.(*http.Request)
return req.FormValue("phoneNumber"), nil
}
mrp.GetInputFunc = func(rq any) ([]byte, error) {
req := rq.(*http.Request)
text := req.FormValue("text")
parts := strings.Split(text, "*")
return []byte(parts[len(parts)-1]), nil
}
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
rqs.Continue = true
rqs.Engine = me
return rqs, nil
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
me.FlushFunc = func(context.Context, io.Writer) (int, error) { return 0, nil }
},
formData: url.Values{
"phoneNumber": []string{"+1234567890"},
"text": []string{"1*2*3"},
},
expectedStatus: http.StatusOK,
expectedBody: "CON ",
},
{
name: "GetSessionId error",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
return "", errors.New("no phone number found")
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
},
formData: url.Values{
"text": []string{"1*2*3"},
},
expectedStatus: http.StatusBadRequest,
expectedBody: "",
},
{
name: "GetInput error",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
req := rq.(*http.Request)
return req.FormValue("phoneNumber"), nil
}
mrp.GetInputFunc = func(rq any) ([]byte, error) {
return nil, errors.New("no input found")
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
},
formData: url.Values{
"phoneNumber": []string{"+1234567890"},
},
expectedStatus: http.StatusBadRequest,
expectedBody: "",
},
{
name: "Process error",
setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
mrp.GetSessionIdFunc = func(rq any) (string, error) {
req := rq.(*http.Request)
return req.FormValue("phoneNumber"), nil
}
mrp.GetInputFunc = func(rq any) ([]byte, error) {
req := rq.(*http.Request)
text := req.FormValue("text")
parts := strings.Split(text, "*")
return []byte(parts[len(parts)-1]), nil
}
mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
return rqs, handlers.ErrStorage
}
mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
},
formData: url.Values{
"phoneNumber": []string{"+1234567890"},
"text": []string{"1*2*3"},
},
expectedStatus: http.StatusInternalServerError,
expectedBody: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockHandler := &httpmocks.MockRequestHandler{}
mockRequestParser := &httpmocks.MockRequestParser{}
mockEngine := &httpmocks.MockEngine{}
tt.setupMocks(mockHandler, mockRequestParser, mockEngine)
ash := NewATSessionHandler(mockHandler)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
ash.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
if tt.expectedBody != "" && w.Body.String() != tt.expectedBody {
t.Errorf("Expected body %q, got %q", tt.expectedBody, w.Body.String())
}
})
}
}
func TestATSessionHandler_Output(t *testing.T) {
tests := []struct {
name string
input handlers.RequestSession
expectedPrefix string
expectedError bool
}{
{
name: "Continue true",
input: handlers.RequestSession{
Continue: true,
Engine: &httpmocks.MockEngine{
FlushFunc: func(context.Context, io.Writer) (int, error) {
return 0, nil
},
},
Writer: &httpmocks.MockWriter{},
},
expectedPrefix: "CON ",
expectedError: false,
},
{
name: "Continue false",
input: handlers.RequestSession{
Continue: false,
Engine: &httpmocks.MockEngine{
FlushFunc: func(context.Context, io.Writer) (int, error) {
return 0, nil
},
},
Writer: &httpmocks.MockWriter{},
},
expectedPrefix: "END ",
expectedError: false,
},
{
name: "Flush error",
input: handlers.RequestSession{
Continue: true,
Engine: &httpmocks.MockEngine{
FlushFunc: func(context.Context, io.Writer) (int, error) {
return 0, errors.New("write error")
},
},
Writer: &httpmocks.MockWriter{},
},
expectedPrefix: "CON ",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ash := &ATSessionHandler{}
_, err := ash.Output(tt.input)
if tt.expectedError && err == nil {
t.Error("Expected an error, but got nil")
}
if !tt.expectedError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
mw := tt.input.Writer.(*httpmocks.MockWriter)
if !mw.WriteStringCalled {
t.Error("WriteString was not called")
}
if mw.WrittenString != tt.expectedPrefix {
t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString)
}
})
}
}
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)
}
})
}
}

View File

@ -1,15 +0,0 @@
package models
import (
"encoding/json"
)
type AccountResponse struct {
Ok bool `json:"ok"`
Result struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
} `json:"result"`
}

View File

@ -1,12 +0,0 @@
package models
import "encoding/json"
type BalanceResponse struct {
Ok bool `json:"ok"`
Result struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
} `json:"result"`
}

View File

@ -1,20 +0,0 @@
package models
import (
"encoding/json"
"time"
)
type TrackStatusResponse struct {
Ok bool `json:"ok"`
Result struct {
Transaction struct {
CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"`
TransferValue json.Number `json:"transferValue"`
TxHash string `json:"txHash"`
TxType string `json:"txType"`
}
} `json:"result"`
}

47
internal/storage/db.go Normal file
View File

@ -0,0 +1,47 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
)
const (
DATATYPE_USERSUB = 64
)
// PrefixDb interface abstracts the database operations.
type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error)
Put(ctx context.Context, key []byte, val []byte) error
}
var _ PrefixDb = (*SubPrefixDb)(nil)
type SubPrefixDb struct {
store db.Db
pfx []byte
}
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{
store: store,
pfx: pfx,
}
}
func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
return s.store.Get(ctx, key)
}
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
}

View File

@ -0,0 +1,54 @@
package storage
import (
"bytes"
"context"
"testing"
memdb "git.defalsify.org/vise.git/db/mem"
)
func TestSubPrefix(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
if err != nil {
t.Fatal(err)
}
r, err := sdba.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
sdbb := NewSubPrefixDb(db, []byte("lala"))
r, err = sdbb.Get(ctx, []byte("foo"))
if err == nil {
t.Fatal("expected not found")
}
err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
if err != nil {
t.Fatal(err)
}
r, err = sdbb.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("pu")) {
t.Fatalf("expected 'pu', got %s", r)
}
r, err = sdba.Get(ctx, []byte("foo"))
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
}

116
internal/storage/gdbm.go Normal file
View File

@ -0,0 +1,116 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
)
var (
dbC map[string]chan db.Db
)
type ThreadGdbmDb struct {
db db.Db
connStr string
}
func NewThreadGdbmDb() *ThreadGdbmDb {
if dbC == nil {
dbC = make(map[string]chan db.Db)
}
return &ThreadGdbmDb{}
}
func(tdb *ThreadGdbmDb) Connect(ctx context.Context, connStr string) error {
var ok bool
_, ok = dbC[connStr]
if ok {
logg.WarnCtxf(ctx, "already registered thread gdbm, skipping", "connStr", connStr)
return nil
}
gdb := gdbmdb.NewGdbmDb()
err := gdb.Connect(ctx, connStr)
if err != nil {
return err
}
dbC[connStr] = make(chan db.Db, 1)
dbC[connStr]<- gdb
tdb.connStr = connStr
return nil
}
func(tdb *ThreadGdbmDb) reserve() {
if tdb.db == nil {
tdb.db = <-dbC[tdb.connStr]
}
}
func(tdb *ThreadGdbmDb) release() {
if tdb.db == nil {
return
}
dbC[tdb.connStr] <- tdb.db
tdb.db = nil
}
func(tdb *ThreadGdbmDb) SetPrefix(pfx uint8) {
tdb.reserve()
tdb.db.SetPrefix(pfx)
}
func(tdb *ThreadGdbmDb) SetSession(sessionId string) {
tdb.reserve()
tdb.db.SetSession(sessionId)
}
func(tdb *ThreadGdbmDb) SetLanguage(lng *lang.Language) {
tdb.reserve()
tdb.db.SetLanguage(lng)
}
func(tdb *ThreadGdbmDb) Safe() bool {
tdb.reserve()
v := tdb.db.Safe()
tdb.release()
return v
}
func(tdb *ThreadGdbmDb) Prefix() uint8 {
tdb.reserve()
v := tdb.db.Prefix()
tdb.release()
return v
}
func(tdb *ThreadGdbmDb) SetLock(typ uint8, locked bool) error {
tdb.reserve()
err := tdb.db.SetLock(typ, locked)
tdb.release()
return err
}
func(tdb *ThreadGdbmDb) Put(ctx context.Context, key []byte, val []byte) error {
tdb.reserve()
err := tdb.db.Put(ctx, key, val)
tdb.release()
return err
}
func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) {
tdb.reserve()
v, err := tdb.db.Get(ctx, key)
tdb.release()
return v, err
}
func(tdb *ThreadGdbmDb) Close() error {
tdb.reserve()
close(dbC[tdb.connStr])
delete(dbC, tdb.connStr)
err := tdb.db.Close()
tdb.db = nil
return err
}

View File

@ -0,0 +1,152 @@
package storage
import (
"context"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
"git.defalsify.org/vise.git/db/postgres"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/initializers"
)
var (
logg = logging.NewVanilla().WithDomain("storage")
)
type StorageService interface {
GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) db.Db
GetResource(ctx context.Context) (resource.Resource, error)
EnsureDbDir() error
}
type MenuStorageService struct {
dbDir string
resourceDir string
resourceStore db.Db
stateStore db.Db
userDataStore db.Db
}
func buildConnStr() string {
host := initializers.GetEnv("DB_HOST", "localhost")
user := initializers.GetEnv("DB_USER", "postgres")
password := initializers.GetEnv("DB_PASSWORD", "")
dbName := initializers.GetEnv("DB_NAME", "")
port := initializers.GetEnv("DB_PORT", "5432")
return fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s",
user, password, host, port, dbName,
)
}
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
return &MenuStorageService{
dbDir: dbDir,
resourceDir: resourceDir,
}
}
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
database, ok := ctx.Value("Database").(string)
if !ok {
return nil, fmt.Errorf("failed to select the database")
}
if existingDb != nil {
return existingDb, nil
}
var newDb db.Db
var err error
if database == "postgres" {
newDb = postgres.NewPgDb()
connStr := buildConnStr()
err = newDb.Connect(ctx, connStr)
} else {
newDb = NewThreadGdbmDb()
storeFile := path.Join(ms.dbDir, fileName)
err = newDb.Connect(ctx, storeFile)
}
if err != nil {
return nil, err
}
return newDb, nil
}
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
stateStore, err := ms.GetStateStore(ctx)
if err != nil {
return nil, err
}
pr := persist.NewPersister(stateStore)
logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", stateStore)
return pr, nil
}
func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
if ms.userDataStore != nil {
return ms.userDataStore, nil
}
userDataStore, err := ms.getOrCreateDb(ctx, ms.userDataStore, "userdata.gdbm")
if err != nil {
return nil, err
}
ms.userDataStore = userDataStore
return ms.userDataStore, nil
}
func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resource, error) {
ms.resourceStore = fsdb.NewFsDb()
err := ms.resourceStore.Connect(ctx, ms.resourceDir)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(ms.resourceStore)
return rfs, nil
}
func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) {
if ms.stateStore != nil {
return ms.stateStore, nil
}
stateStore, err := ms.getOrCreateDb(ctx, ms.stateStore, "state.gdbm")
if err != nil {
return nil, err
}
ms.stateStore = stateStore
return ms.stateStore, nil
}
func (ms *MenuStorageService) EnsureDbDir() error {
err := os.MkdirAll(ms.dbDir, 0700)
if err != nil {
return fmt.Errorf("state dir create exited with error: %v\n", err)
}
return nil
}
func (ms *MenuStorageService) Close() error {
errA := ms.stateStore.Close()
errB := ms.userDataStore.Close()
errC := ms.resourceStore.Close()
if errA != nil || errB != nil || errC != nil {
return fmt.Errorf("%v %v %v", errA, errB, errC)
}
return nil
}

View File

@ -0,0 +1,124 @@
package testutil
import (
"context"
"fmt"
"os"
"path"
"time"
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
testdataloader "github.com/peteole/testdata-loader"
"git.grassecon.net/urdt/ussd/remote"
)
var (
baseDir = testdataloader.GetBasePath()
logg = logging.NewVanilla()
scriptDir = path.Join(baseDir, "services", "registration")
)
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", "gdbm")
pfp := path.Join(scriptDir, "pp.csv")
var eventChannel = make(chan bool)
cfg := engine.Config{
Root: "root",
SessionId: sessionId,
OutputSize: uint32(160),
FlagCount: uint32(128),
}
dbDir := ".test_state"
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
err := menuStorageService.EnsureDbDir()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
rs, err := menuStorageService.GetResource(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
pe, err := menuStorageService.GetPersister(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
dbResource, ok := rs.(*resource.DbResource)
if !ok {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
lhs.SetDataStore(&userDataStore)
lhs.SetPersister(pe)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
if testtag.AccountService == nil {
testtag.AccountService = &remote.AccountService{}
}
switch testtag.AccountService.(type) {
case *testservice.TestAccountService:
go func() {
eventChannel <- false
}()
case *remote.AccountService:
go func() {
time.Sleep(5 * time.Second) // Wait for 5 seconds
eventChannel <- true
}()
default:
panic("Unknown account service type")
}
hl, err := lhs.GetHandler(testtag.AccountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
en := lhs.GetEngine()
en = en.WithFirst(hl.Init)
cleanFn := func() {
err := en.Finish()
if err != nil {
logg.Errorf(err.Error())
}
err = menuStorageService.Close()
if err != nil {
logg.Errorf(err.Error())
}
logg.Infof("testengine storage closed")
}
return en, cleanFn, eventChannel
}

View File

@ -0,0 +1,111 @@
package driver
import (
"encoding/json"
"log"
"os"
"regexp"
)
type Step struct {
Input string `json:"input"`
ExpectedContent string `json:"expectedContent"`
}
func (s *Step) MatchesExpectedContent(content []byte) (bool, error) {
pattern := regexp.QuoteMeta(s.ExpectedContent)
re, err := regexp.Compile(pattern)
if err != nil {
return false, err
}
if re.Match([]byte(content)) {
return true, nil
}
return false, nil
}
// Group represents a group of steps
type Group struct {
Name string `json:"name"`
Steps []Step `json:"steps"`
}
type TestCase struct {
Name string
Input string
ExpectedContent string
}
func (s *TestCase) MatchesExpectedContent(content []byte) (bool, error) {
pattern := regexp.QuoteMeta(s.ExpectedContent)
re, err := regexp.Compile(pattern)
if err != nil {
return false, err
}
// Check if the content matches the regex pattern
if re.Match(content) {
return true, nil
}
return false, nil
}
// DataGroup represents the overall structure of the JSON.
type DataGroup struct {
Groups []Group `json:"groups"`
}
type Session struct {
Name string `json:"name"`
Groups []Group `json:"groups"`
}
func ReadData() []Session {
data, err := os.ReadFile("test_setup.json")
if err != nil {
log.Fatalf("Failed to read file: %v", err)
}
// Unmarshal JSON data
var sessions []Session
err = json.Unmarshal(data, &sessions)
if err != nil {
log.Fatalf("Failed to unmarshal JSON: %v", err)
}
return sessions
}
func FilterGroupsByName(groups []Group, name string) []Group {
var filteredGroups []Group
for _, group := range groups {
if group.Name == name {
filteredGroups = append(filteredGroups, group)
}
}
return filteredGroups
}
func LoadTestGroups(filePath string) (DataGroup, error) {
var sessionsData DataGroup
data, err := os.ReadFile(filePath)
if err != nil {
return sessionsData, err
}
err = json.Unmarshal(data, &sessionsData)
return sessionsData, err
}
func CreateTestCases(group DataGroup) []TestCase {
var tests []TestCase
for _, group := range group.Groups {
for _, step := range group.Steps {
// Create a test case for each group
tests = append(tests, TestCase{
Name: group.Name,
Input: step.Input,
ExpectedContent: step.ExpectedContent,
})
}
}
return tests
}

View File

@ -0,0 +1,30 @@
package httpmocks
import (
"context"
"io"
)
// MockEngine implements the engine.Engine interface for testing
type MockEngine struct {
InitFunc func(context.Context) (bool, error)
ExecFunc func(context.Context, []byte) (bool, error)
FlushFunc func(context.Context, io.Writer) (int, error)
FinishFunc func() error
}
func (m *MockEngine) Init(ctx context.Context) (bool, error) {
return m.InitFunc(ctx)
}
func (m *MockEngine) Exec(ctx context.Context, input []byte) (bool, error) {
return m.ExecFunc(ctx, input)
}
func (m *MockEngine) Flush(ctx context.Context, w io.Writer) (int, error) {
return m.FlushFunc(ctx, w)
}
func (m *MockEngine) Finish() error {
return m.FinishFunc()
}

View File

@ -0,0 +1,47 @@
package httpmocks
import (
"git.defalsify.org/vise.git/engine"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/internal/handlers"
)
// MockRequestHandler implements handlers.RequestHandler interface for testing
type MockRequestHandler struct {
ProcessFunc func(handlers.RequestSession) (handlers.RequestSession, error)
GetConfigFunc func() engine.Config
GetEngineFunc func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
OutputFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
ResetFunc func(rs handlers.RequestSession) (handlers.RequestSession, error)
ShutdownFunc func()
GetRequestParserFunc func() handlers.RequestParser
}
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
return m.ProcessFunc(rqs)
}
func (m *MockRequestHandler) GetConfig() engine.Config {
return m.GetConfigFunc()
}
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
return m.GetEngineFunc(cfg, rs, pe)
}
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
return m.OutputFunc(rs)
}
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
return m.ResetFunc(rs)
}
func (m *MockRequestHandler) Shutdown() {
m.ShutdownFunc()
}
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
return m.GetRequestParserFunc()
}

View File

@ -0,0 +1,15 @@
package httpmocks
// MockRequestParser implements the handlers.RequestParser interface for testing
type MockRequestParser struct {
GetSessionIdFunc func(any) (string, error)
GetInputFunc func(any) ([]byte, error)
}
func (m *MockRequestParser) GetSessionId(rq any) (string, error) {
return m.GetSessionIdFunc(rq)
}
func (m *MockRequestParser) GetInput(rq any) ([]byte, error) {
return m.GetInputFunc(rq)
}

View File

@ -0,0 +1,25 @@
package httpmocks
import "net/http"
// MockWriter implements a mock io.Writer for testing
type MockWriter struct {
WriteStringCalled bool
WrittenString string
}
func (m *MockWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
func (m *MockWriter) WriteString(s string) (n int, err error) {
m.WriteStringCalled = true
m.WrittenString = s
return len(s), nil
}
func (m *MockWriter) Header() http.Header {
return http.Header{}
}
func (m *MockWriter) WriteHeader(statusCode int) {}

View File

@ -0,0 +1,45 @@
package mocks
import (
"context"
"git.grassecon.net/urdt/ussd/models"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"github.com/stretchr/testify/mock"
)
// MockAccountService implements AccountServiceInterface for testing
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
args := m.Called()
return args.Get(0).(*models.AccountResult), args.Error(1)
}
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResult), args.Error(1)
}
func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) {
args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResult), args.Error(1)
}
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
args := m.Called(publicKey)
return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1)
}
func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
args := m.Called(publicKey)
return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1)
}
func(m MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
args := m.Called(address)
return args.Get(0).(*models.VoucherDataResult), args.Error(1)
}

View File

@ -0,0 +1,52 @@
package testservice
import (
"context"
"encoding/json"
"git.grassecon.net/urdt/ussd/models"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
type TestAccountService struct {
}
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
return &models.AccountResult {
TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
}, nil
}
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
balanceResponse := &models.BalanceResult {
Balance: "0.003 CELO",
Nonce: json.Number("0"),
}
return balanceResponse, nil
}
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
return &models.TrackStatusResult {
Active: true,
}, nil
}
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
return []dataserviceapi.TokenHoldings {
dataserviceapi.TokenHoldings {
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF",
TokenDecimals: "6",
Balance: "2745987",
},
}, nil
}
func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
return []dataserviceapi.Last10TxResponse{}, nil
}
func(m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
return &models.VoucherDataResult{}, nil
}

View File

@ -0,0 +1,12 @@
// +build !online
package testtag
import (
"git.grassecon.net/urdt/ussd/remote"
accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
)
var (
AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{}
)

View File

@ -0,0 +1,9 @@
// +build online
package testtag
import "git.grassecon.net/urdt/ussd/internal/handlers/server"
var (
AccountService server.AccountServiceInterface
)

View File

@ -0,0 +1,51 @@
package utils
import (
"context"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla().WithDomain("adminstore")
)
type AdminStore struct {
ctx context.Context
FsStore db.Db
}
func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) {
fsStore, err := getFsStore(ctx, fileName)
if err != nil {
return nil, err
}
return &AdminStore{ctx: ctx, FsStore: fsStore}, nil
}
func getFsStore(ctx context.Context, connectStr string) (db.Db, error) {
fsStore := fsdb.NewFsDb()
err := fsStore.Connect(ctx, connectStr)
fsStore.SetPrefix(db.DATATYPE_USERDATA)
if err != nil {
return nil, err
}
return fsStore, nil
}
// Checks if the given sessionId is listed as an admin.
func (as *AdminStore) IsAdmin(sessionId string) (bool, error) {
_, err := as.FsStore.Get(as.ctx, []byte(sessionId))
if err != nil {
if db.IsNotFound(err) {
logg.Printf(logging.LVL_INFO, "Returning false because session id was not found")
return false, nil
} else {
return false, err
}
}
return true, nil
}

11
internal/utils/isocode.go Normal file
View File

@ -0,0 +1,11 @@
package utils
var isoCodes = map[string]bool{
"eng": true, // English
"swa": true, // Swahili
}
func IsValidISO639(code string) bool {
return isoCodes[code]
}

View File

@ -0,0 +1,460 @@
{
"groups": [
{
"name": "my_account_change_pin",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "5",
"expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your old PIN\n\n0:Back"
},
{
"input": "1234",
"expectedContent": "Enter a new four number PIN:\n\n0:Back"
},
{
"input": "1234",
"expectedContent": "Confirm your new PIN:\n\n0:Back"
},
{
"input": "1234",
"expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_language_change",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "2",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Select language:\n0:english\n1:kiswahili"
},
{
"input": "0",
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_check_my_balance",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "3",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_check_community_balance",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "3",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "2",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_firstname",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your first names:\n0:Back"
},
{
"input": "foo",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_familyname",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_gender",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "3",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_yob",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "4",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1945",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_location",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "5",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_edit_offerings",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "6",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
{
"name": "menu_my_account_view_profile",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "7",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,380 @@
package menutraversaltest
import (
"bytes"
"context"
"log"
"math/rand"
"os"
"regexp"
"testing"
"git.grassecon.net/urdt/ussd/internal/testutil"
"git.grassecon.net/urdt/ussd/internal/testutil/driver"
"github.com/gofrs/uuid"
)
var (
testData = driver.ReadData()
testStore = ".test_state"
groupTestFile = "group_test.json"
sessionID string
src = rand.NewSource(42)
g = rand.New(src)
)
func GenerateSessionId() string {
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4()
if err != nil {
panic(err)
}
return v.String()
}
// Extract the public key from the engine response
func extractPublicKey(response []byte) string {
// Regex pattern to match the public key starting with 0x and 40 characters
re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`)
match := re.Find(response)
if match != nil {
return string(match)
}
return ""
}
// Extracts the balance value from the engine response.
func extractBalance(response []byte) string {
// Regex to match "Balance: <amount> <symbol>" followed by a newline
re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
}
return ""
}
// Extracts the Maximum amount value from the engine response.
func extractMaxAmount(response []byte) string {
// Regex to match "Maximum amount: <amount>" followed by a newline
re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // "<amount>"
}
return ""
}
// Extracts the send amount value from the engine response.
func extractSendAmount(response []byte) string {
// Regex to match the pattern "will receive X.XX SYM from"
re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // Returns "X.XX SYM"
}
return ""
}
func TestMain(m *testing.M) {
sessionID = GenerateSessionId()
defer func() {
if err := os.RemoveAll(testStore); err != nil {
log.Fatalf("Failed to delete state store %s: %v", testStore, err)
}
}()
m.Run()
}
func TestAccountCreationSuccessful(t *testing.T) {
en, fn, eventChannel := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
_, err = en.Flush(ctx, w)
if err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
<-eventChannel
}
func TestAccountRegistrationRejectTerms(t *testing.T) {
// Generate a new UUID for this edge case test
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4()
if err != nil {
t.Fail()
}
edgeCaseSessionID := v.String()
en, fn, _ := testutil.TestEngine(edgeCaseSessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestMainMenuHelp(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "main_menu_help")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestMainMenuQuit(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestMyAccount_MyAddress(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address")
for _, group := range groups {
for index, step := range group.Steps {
t.Logf("step %v with input %v", index, step.Input)
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
publicKey := extractPublicKey(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b)
}
}
}
}
}
func TestMainMenuSend(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
max_amount := extractMaxAmount(b)
send_amount := extractSendAmount(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile)
if err != nil {
log.Fatalf("Failed to load test groups: %v", err)
}
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
// Create test cases from loaded groups
tests := driver.CreateTestCases(groups)
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
cont, err := en.Exec(ctx, []byte(tt.Input))
if err != nil {
t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err)
return
}
if !cont {
return
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(tt.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
tt.ExpectedContent = string(expectedContent)
match, err := tt.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
}
})
}
}

View File

@ -0,0 +1,153 @@
[
{
"name": "session one",
"groups": [
{
"name": "account_creation_successful",
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
},
{
"input": "0",
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
},
{
"input": "1234",
"expectedContent": "Enter your four number PIN again:"
},
{
"input": "1111",
"expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter your four number PIN again:"
},
{
"input": "1234",
"expectedContent": "Your account is being created...Thank you for using Sarafu. Goodbye!"
}
]
},
{
"name": "account_creation_reject_terms",
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
},
{
"input": "1",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
},
{
"name": "send_with_invalid_inputs",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back"
},
{
"input": "000",
"expectedContent": "000 is not registered or invalid, please try again:\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back"
},
{
"input": "065656",
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "10000000",
"expectedContent": "Amount 10000000 is invalid, please try again:\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "1.00",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1222",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}."
}
]
},
{
"name": "main_menu_help",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "4",
"expectedContent": "For more help,please call: 0757628885"
}
]
},
{
"name": "main_menu_quit",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "9",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
},
{
"name": "menu_my_account_my_address",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "6",
"expectedContent": "Address: {public_key}\n0:Back\n9:Quit"
},
{
"input": "9",
"expectedContent": "Thank you for using Sarafu. Goodbye!"
}
]
}
]
}
]

View File

@ -0,0 +1,6 @@
package models
type AccountResult struct {
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}

View File

@ -0,0 +1,9 @@
package models
import "encoding/json"
type BalanceResult struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}

18
models/tokenresponse.go Normal file
View File

@ -0,0 +1,18 @@
package models
type ApiResponse struct {
OK bool `json:"ok"`
Description string `json:"description"`
Result Result `json:"result"`
}
type Result struct {
Holdings []Holding `json:"holdings"`
}
type Holding struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}

View File

@ -0,0 +1,18 @@
package models
import (
"encoding/json"
"time"
)
type Transaction struct {
CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"`
TransferValue json.Number `json:"transferValue"`
TxHash string `json:"txHash"`
TxType string `json:"txType"`
}
type TrackStatusResult struct {
Active bool `json:"active"`
}

View File

@ -0,0 +1,21 @@
package models
import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
//type VoucherHoldingResponse struct {
// Ok bool `json:"ok"`
// Description string `json:"description"`
// Result VoucherResult `json:"result"`
//}
// VoucherResult holds the list of token holdings
type VoucherResult struct {
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
}
type VoucherDataResult struct {
TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
SinkAddress string `json:"sinkAddress"`
}

243
remote/accountservice.go Normal file
View File

@ -0,0 +1,243 @@
package remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"net/url"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
var (
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
type AccountServiceInterface interface {
CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error)
CreateAccount(ctx context.Context) (*models.AccountResult, error)
TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error)
FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error)
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
}
type AccountService struct {
}
// Parameters:
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
// AccountResponse struct can be used here to check the account status during a transaction.
//
// Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil
func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
var r models.TrackStatusResult
ep, err := url.JoinPath(config.TrackURL, publicKey)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil {
return nil, err
}
_, err = doCustodialRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return &r, nil
}
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
var balanceResult models.BalanceResult
ep, err := url.JoinPath(config.BalanceURL, publicKey)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil {
return nil, err
}
_, err = doCustodialRequest(ctx, req, &balanceResult)
return &balanceResult, err
}
// CreateAccount creates a new account in the custodial system.
// Returns:
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
// If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
var r models.AccountResult
// Create a new request
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
if err != nil {
return nil, err
}
_, err = doCustodialRequest(ctx, req, &r)
if err != nil {
log.Printf("Failed to make custodial %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
return nil, err
}
return &r, nil
}
// FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
var r []dataserviceapi.TokenHoldings
ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil {
return nil, err
}
_, err = doDataRequest(ctx, req, r)
if err != nil {
return nil, err
}
return r, nil
}
// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
var r []dataserviceapi.Last10TxResponse
ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil {
return nil, err
}
_, err = doDataRequest(ctx, req, r)
if err != nil {
return nil, err
}
return r, nil
}
// VoucherData retrieves voucher metadata from the data indexer API endpoint.
// Parameters:
// - address: The voucher address.
func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
var voucherDataResult models.VoucherDataResult
ep, err := url.JoinPath(config.VoucherDataURL, address)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil {
return nil, err
}
_, err = doCustodialRequest(ctx, req, &voucherDataResult)
return &voucherDataResult, err
}
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
var okResponse api.OKResponse
var errResponse api.ErrResponse
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
errResponse.Description = err.Error()
return nil, err
}
defer resp.Body.Close()
InfoLogger.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type"))
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusBadRequest {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
return nil, err
}
return nil, errors.New(errResponse.Description)
}
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
return nil, err
}
if len(okResponse.Result) == 0 {
return nil, errors.New("Empty api result")
}
v, err := json.Marshal(okResponse.Result)
if err != nil {
return nil, err
}
err = json.Unmarshal(v, &rcpt)
return &okResponse, err
}
func doCustodialRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.CustodialAPIKey)
logRequestDetails(req)
return doRequest(ctx, req, rcpt)
}
func doDataRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.DataAPIKey)
logRequestDetails(req)
return doRequest(ctx, req, rcpt)
}
func logRequestDetails(req *http.Request) {
var bodyBytes []byte
contentType := req.Header.Get("Content-Type")
if req.Body != nil {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
ErrorLogger.Printf("Error reading request body: %s", err)
return
}
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
} else {
bodyBytes = []byte("-")
}
InfoLogger.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
}

44
sample_tokens.json Normal file
View File

@ -0,0 +1,44 @@
{
"ok": true,
"description": "Token holdings with current balances",
"result": {
"holdings": [
{
"contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
"tokenSymbol": "FSPTST",
"tokenDecimals": "6",
"balance": "8869964242"
},
{
"contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA",
"tokenSymbol": "GEO",
"tokenDecimals": "6",
"balance": "9884"
},
{
"contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273",
"tokenSymbol": "MFNK",
"tokenDecimals": "6",
"balance": "19788697"
},
{
"contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83",
"tokenSymbol": "MILO",
"tokenDecimals": "6",
"balance": "75"
},
{
"contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
"tokenSymbol": "SOHAIL",
"tokenDecimals": "6",
"balance": "27874115"
},
{
"contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958",
"tokenSymbol": "SRF",
"tokenDecimals": "6",
"balance": "2745987"
}
]
}
}

View File

@ -1,10 +1,11 @@
# Variables to match files in the current directory # Variables to match files in the current directory
INPUTS = $(wildcard ./*.vis) INPUTS = $(wildcard ./*.vis)
TXTS = $(wildcard ./*.txt.orig) TXTS = $(wildcard ./*.txt.orig)
VISE_PATH := ../../go-vise
# Rule to build .bin files from .vis files # Rule to build .bin files from .vis files
%.vis: %.vis:
go run ../../go-vise/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin go run $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin
@echo "Built $(basename $@).bin from $(basename $@).vis" @echo "Built $(basename $@).bin from $(basename $@).vis"
# Rule to copy .orig files to .txt # Rule to copy .orig files to .txt

View File

@ -0,0 +1 @@
Something went wrong.Please try again

View File

@ -0,0 +1 @@
HALT

View File

@ -1,4 +1,4 @@
RELOAD verify_pin RELOAD verify_create_pin
CATCH create_pin_mismatch flag_pin_mismatch 1 CATCH create_pin_mismatch flag_pin_mismatch 1
LOAD quit 0 LOAD quit 0
HALT HALT

View File

@ -1,6 +1,8 @@
LOAD check_identifier 0 LOAD check_identifier 0
RELOAD check_identifier RELOAD check_identifier
MAP check_identifier MAP check_identifier
MOUT back 0
MOUT quit 9 MOUT quit 9
HALT HALT
INCMP _ 0
INCMP quit 9 INCMP quit 9

View File

@ -0,0 +1 @@
Anwani:{{.check_identifier}}

View File

@ -1,12 +1,15 @@
LOAD reset_transaction_amount 0 LOAD reset_transaction_amount 0
LOAD max_amount 10 LOAD max_amount 10
RELOAD max_amount
MAP max_amount MAP max_amount
MOUT back 0 MOUT back 0
HALT HALT
LOAD validate_amount 64 LOAD validate_amount 64
RELOAD validate_amount RELOAD validate_amount
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1 CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0 INCMP _ 0
LOAD get_recipient 12 LOAD get_recipient 12
LOAD get_sender 64 LOAD get_sender 64
LOAD get_amount 32
INCMP transaction_pin * INCMP transaction_pin *

View File

@ -0,0 +1 @@
Failed to connect to the custodial service.Please try again.

View File

@ -0,0 +1,5 @@
MOUT retry 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9

View File

@ -1,4 +1,5 @@
LOAD reset_account_authorized 0 LOAD reset_account_authorized 0
RELOAD reset_account_authorized
MOUT my_balance 1 MOUT my_balance 1
MOUT community_balance 2 MOUT community_balance 2
MOUT back 0 MOUT back 0

View File

@ -1 +1 @@
Salio Salio:

View File

@ -0,0 +1 @@
Select language:

View File

@ -0,0 +1,10 @@
LOAD reset_account_authorized 0
LOAD reset_incorrect 0
CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0
MOUT english 0
MOUT kiswahili 1
HALT
INCMP set_default 0
INCMP set_swa 1
INCMP . *

View File

@ -0,0 +1 @@
Chagua lugha:

View File

@ -1,2 +1 @@
Your community balance is: 0.00SRF {{.fetch_custodial_balances}}

View File

@ -1,5 +1,11 @@
LOAD reset_incorrect 0 LOAD reset_incorrect 6
LOAD fetch_custodial_balances 0
CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances
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
LOAD quit_with_balance 0 MOUT back 0
MOUT quit 9
HALT HALT
INCMP _ 0
INCMP quit 9

View File

@ -1,4 +1,4 @@
LOAD save_pin 0 LOAD save_temporary_pin 6
HALT HALT
LOAD verify_pin 8 LOAD verify_create_pin 8
INCMP account_creation * INCMP account_creation *

View File

@ -0,0 +1 @@
Please confirm new PIN for:{{.retrieve_blocked_number}}

View File

@ -0,0 +1,14 @@
CATCH pin_entry flag_incorrect_pin 1
RELOAD retrieve_blocked_number
MAP retrieve_blocked_number
CATCH invalid_others_pin flag_valid_pin 0
CATCH pin_reset_result flag_account_authorized 1
LOAD save_others_temporary_pin 6
RELOAD save_others_temporary_pin
MOUT back 0
HALT
INCMP _ 0
LOAD check_pin_mismatch 0
RELOAD check_pin_mismatch
CATCH others_pin_mismatch flag_pin_mismatch 1
INCMP pin_entry *

View File

@ -0,0 +1 @@
Tafadhali thibitisha PIN mpya ya: {{.retrieve_blocked_number}}

View File

@ -3,5 +3,3 @@ MOUT back 0
HALT HALT
INCMP _ 0 INCMP _ 0
INCMP * pin_reset_success INCMP * pin_reset_success

View File

@ -2,8 +2,8 @@ LOAD create_account 0
CATCH account_creation_failed flag_account_creation_failed 1 CATCH account_creation_failed flag_account_creation_failed 1
MOUT exit 0 MOUT exit 0
HALT HALT
LOAD save_pin 0 LOAD save_temporary_pin 6
RELOAD save_pin RELOAD save_temporary_pin
CATCH . flag_incorrect_pin 1 CATCH . flag_incorrect_pin 1
INCMP quit 0 INCMP quit 0
INCMP confirm_create_pin * INCMP confirm_create_pin *

View File

@ -0,0 +1 @@
Edit family name

View File

@ -0,0 +1 @@
Weka jina la familia

View File

@ -1,20 +1,21 @@
LOAD reset_account_authorized 16 LOAD reset_account_authorized 16
RELOAD reset_account_authorized
LOAD reset_allow_update 0 LOAD reset_allow_update 0
RELOAD reset_allow_update RELOAD reset_allow_update
MOUT edit_name 1 MOUT edit_name 1
MOUT edit_gender 2 MOUT edit_familyname 2
MOUT edit_yob 3 MOUT edit_gender 3
MOUT edit_location 4 MOUT edit_yob 4
MOUT edit_offerings 5 MOUT edit_location 5
MOUT view 6 MOUT edit_offerings 6
MOUT view 7
MOUT back 0 MOUT back 0
HALT HALT
INCMP _ 0 INCMP my_account 0
LOAD set_reset_single_edit 0
RELOAD set_reset_single_edit
INCMP enter_name 1 INCMP enter_name 1
INCMP select_gender 2 INCMP enter_familyname 2
INCMP enter_yob 3 INCMP select_gender 3
INCMP enter_location 4 INCMP enter_yob 4
INCMP enter_offerings 5 INCMP enter_location 5
INCMP view_profile 6 INCMP enter_offerings 6
INCMP view_profile 7

View File

@ -1,5 +1,8 @@
LOAD save_firstname 0 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_familyname 0
RELOAD save_familyname
INCMP _ 0 INCMP _ 0
INCMP select_gender * INCMP pin_entry *

View File

@ -0,0 +1 @@
Weka jina la familia

View File

@ -1,9 +1,8 @@
CATCH incorrect_date_format flag_incorrect_date_format 1 CATCH incorrect_pin flag_incorrect_pin 1
LOAD save_yob 0 CATCH update_location flag_allow_update 1
CATCH update_success flag_allow_update 1
MOUT back 0 MOUT back 0
HALT HALT
INCMP _ 0
LOAD save_location 0 LOAD save_location 0
CATCH pin_entry flag_single_edit 1 RELOAD save_location
INCMP enter_offerings * INCMP _ 0
INCMP pin_entry *

View File

@ -1,4 +1,8 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_firstname flag_allow_update 1
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_firstname 0
RELOAD save_firstname
INCMP _ 0 INCMP _ 0
INCMP enter_familyname * INCMP pin_entry *

View File

@ -1,8 +1,8 @@
LOAD save_location 0
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_success flag_allow_update 1 CATCH update_offerings flag_allow_update 1
LOAD save_offerings 0
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_offerings 0 RELOAD save_offerings
INCMP _ 0 INCMP _ 0
INCMP pin_entry * INCMP pin_entry *

View File

@ -0,0 +1 @@
Enter other's phone number:

View File

@ -0,0 +1,7 @@
CATCH no_admin_privilege flag_admin_privilege 0
LOAD reset_account_authorized 0
RELOAD reset_account_authorized
MOUT back 0
HALT
INCMP _ 0
INCMP enter_others_new_pin *

View File

@ -0,0 +1 @@
Weka nambari ya simu ili kutuma ombi la kubadilisha nambari ya siri:

View File

@ -0,0 +1 @@
Please enter new PIN for: {{.retrieve_blocked_number}}

View File

@ -0,0 +1,12 @@
LOAD validate_blocked_number 6
RELOAD validate_blocked_number
CATCH unregistered_number flag_unregistered_number 1
LOAD retrieve_blocked_number 0
RELOAD retrieve_blocked_number
MAP retrieve_blocked_number
MOUT back 0
HALT
LOAD verify_new_pin 6
RELOAD verify_new_pin
INCMP _ 0
INCMP * confirm_others_new_pin

View File

@ -0,0 +1 @@
Tafadhali weka PIN mpya ya: {{.retrieve_blocked_number}}

View File

@ -1,9 +1,10 @@
LOAD save_gender 0 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_success flag_allow_update 1 CATCH update_yob flag_allow_update 1
MOUT back 0 MOUT back 0
HALT HALT
INCMP _ 0 LOAD verify_yob 0
LOAD verify_yob 8 CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 0 LOAD save_yob 0
CATCH pin_entry flag_single_edit 1 RELOAD save_yob
INCMP enter_location * INCMP _ 0
INCMP pin_entry *

View File

@ -1 +0,0 @@
Guard my PIN

View File

@ -1 +0,0 @@
Linda PIN yangu

View File

@ -0,0 +1,2 @@
LOAD quit_with_help 0
HALT

Some files were not shown because too many files have changed in this diff Show More