forked from grassrootseconomics/visedriver
Compare commits
1045 Commits
wip-accoun
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
5228aef088 | ||
|
5bf0a0e858 | ||
|
f13dab9a45 | ||
|
3f8e08151a | ||
|
9e4c65c8b4 | ||
|
611c5a8dfc | ||
|
dcf777bf08 | ||
|
ec4ad6e44b | ||
|
5b312f9569 | ||
|
2d89db3a37 | ||
|
6d8992fe38 | ||
|
5cffbe5cd8 | ||
|
b28a047777 | ||
|
2ea51d88d8 | ||
|
2e0e854c4b | ||
|
c93a07832d | ||
|
46bf21b7b8 | ||
|
dff662663d | ||
|
977d14c529 | ||
|
fd6e5caf53 | ||
|
840c22ca89 | ||
|
f939a20543 | ||
|
8387644019 | ||
|
c535938dbc | ||
|
b60d2648ea | ||
|
85ede15613 | ||
b11f11b5fa | |||
3d35a5de78 | |||
a19ace85f8 | |||
|
a7a8a482ab | ||
|
d8e7c443b5 | ||
|
5216a9383e | ||
|
6cd639fd19 | ||
|
a7ca280964 | ||
8f5ed0cd4f | |||
|
f1664a43a8 | ||
c29abfe21e | |||
9a6d8e5158 | |||
|
9ca5091692 | ||
db431c750e | |||
2b9c6d641e | |||
|
b9712098ef | ||
|
bcf1965a6c | ||
3747f87a7c | |||
1f0568df32 | |||
|
24c513d4f0 | ||
b3fd6f5c1a | |||
73eb765408 | |||
f660f6c19a | |||
3fccfaab61 | |||
|
b50a51df9b | ||
|
df8c9aab0c | ||
|
ddefdd7fb3 | ||
5734011f96 | |||
|
379d98ccd5 | ||
f40e11c267 | |||
b698f08136 | |||
4d7589ad95 | |||
efdb52bccd | |||
2ff9fed3c5 | |||
477b4cf8f6 | |||
ed6651697a | |||
c359d99075 | |||
8d477356f3 | |||
7f3294a8a2 | |||
4b5f08e25e | |||
ea9cab930e | |||
a37f6e6da3 | |||
f59c3a53ef | |||
81c3378ea6 | |||
46a6d2bc6e | |||
|
721f80d0f2 | ||
f49e54a562 | |||
|
5081b6d4ce | ||
4d72ae0313 | |||
4fe64a7747 | |||
3004698d5b | |||
50c7ff1046 | |||
07b061a68b | |||
6339f0c2e5 | |||
|
1fa830f286 | ||
64fba91670 | |||
c15958a1ad | |||
ee442daefa | |||
656052dc74 | |||
6c5873da6f | |||
11d30583a4 | |||
f83f539046 | |||
562bd4fa24 | |||
90df0eefc3 | |||
b37f2a0a11 | |||
68e4c9af03 | |||
c12e867ac3 | |||
79de0a9092 | |||
3ee15497a5 | |||
|
e09e324a50 | ||
|
599815c343 | ||
|
462c0d7677 | ||
80b96e9bf6 | |||
b5561decd1 | |||
|
02823fd64e | ||
|
cd575c2edb | ||
f3d4f35718 | |||
52787bdb4d | |||
|
f8d8f265f1 | ||
|
9371b52f3e | ||
|
563000ec15 | ||
824d39908b | |||
|
52fd1eced2 | ||
a312ea5b84 | |||
|
5c7a535288 | ||
4836162f40 | |||
|
cc2f7b41df | ||
|
d39740a09a | ||
|
2024cc96e2 | ||
|
d2d878d5d7 | ||
c995143543 | |||
44570e20ef | |||
362eb209ef | |||
c69d3896f1 | |||
974af6b2a7 | |||
|
bb4037e73f | ||
|
739fd90dfd | ||
|
6789c4f550 | ||
|
437f73827d | ||
|
f0a4a0df61 | ||
|
bd604219b8 | ||
|
51b6fc0dde | ||
|
cc9760125a | ||
|
3a9f3fa373 | ||
|
89c21847b9 | ||
|
450dfa02cc | ||
|
f61e65f4fe | ||
|
a4d6cef9c0 | ||
|
2992f7ae8e | ||
|
dc61d05584 | ||
|
83857026d3 | ||
|
349051b5ef | ||
47b5ff0435 | |||
|
e92e498726 | ||
|
25867cf05e | ||
|
c3cbe1cd92 | ||
|
418080d093 | ||
|
2e30739ec9 | ||
d5a2680500 | |||
|
dc1674ec55 | ||
|
d950b10b50 | ||
|
bcb3ab905e | ||
|
3ed9caf16d | ||
|
86464c31d2 | ||
5ee10d8e14 | |||
62f3681b9e | |||
3ce1435591 | |||
f65c458daa | |||
|
67007fcd48 | ||
|
f1b258fa6d | ||
|
daec816a3e | ||
|
ac0c43cb43 | ||
|
9013cc3618 | ||
|
056d056613 | ||
|
e581ec4771 | ||
|
e16b7445e8 | ||
|
1b12f0ba5f | ||
d2fce05461 | |||
68ac237449 | |||
162e6c1934 | |||
8bd025f2b2 | |||
9d6e25e184 | |||
c26f5683f6 | |||
91dc9ce82f | |||
0fe48a30fa | |||
|
c1e0617bb3 | ||
|
6723884103 | ||
|
b888af446d | ||
|
43b2c3b78d | ||
|
d67853f6d9 | ||
58edfa01a2 | |||
3830c12a57 | |||
f1fd690a7b | |||
|
06230dc557 | ||
491b7424a9 | |||
29ce4b83bd | |||
ca8df5989a | |||
82b4365d16 | |||
98db85511b | |||
99a4d3ff42 | |||
d95c7abea4 | |||
fd1ac85a1b | |||
c899c098f6 | |||
5ca6a74274 | |||
48d63fb43f | |||
|
6ee2c88fe2 | ||
e666c58644 | |||
e980586910 | |||
ffd5be1f1f | |||
ed1aeecf7d | |||
3b69f3d38d | |||
|
cd58f5ae33 | ||
7a535f796a | |||
7c4c73125e | |||
|
c7dbe1d88f | ||
4ea52bf3fb | |||
be2ea3a2f0 | |||
8217ea8fdc | |||
3c73fc7188 | |||
1311a0cab9 | |||
|
3bcd48e5a7 | ||
|
0e12c0ee4e | ||
3caee98cdb | |||
db7c9bf56d | |||
0a332ec501 | |||
90367fe53e | |||
50c006546c | |||
e8c171a82e | |||
58a60f2c81 | |||
0820e1b9f2 | |||
46edf2b819 | |||
11eb61ba35 | |||
813b92af78 | |||
5579991d66 | |||
f4f4fdd3ac | |||
be215d3f75 | |||
235af3519d | |||
1292851226 | |||
dfd0a0994b | |||
97fcdda12f | |||
055c2db790 | |||
ecfdab47a8 | |||
fda68231ea | |||
d08afff443 | |||
17ba6a06ba | |||
dbd59a4023 | |||
5534706189 | |||
5428626c3f | |||
9b33117cb1 | |||
70b2fa4ac2 | |||
fd6ff86579 | |||
549782f230 | |||
8cf4848b45 | |||
9f6c0a1111 | |||
|
1ab49647f6 | ||
|
8d4d8a48e0 | ||
7aea2af9a1 | |||
5cd791aae7 | |||
df5e5f1a4b | |||
64c1fe5276 | |||
f38ea59569 | |||
6cc285d1e8 | |||
0d7f7aaca1 | |||
f8ea2daa73 | |||
|
c820e89cb7 | ||
e05f8e7291 | |||
2383e8ead3 | |||
1a4ee0d3e1 | |||
6f3b30e2fe | |||
b1e4b63c6a | |||
3129e8210e | |||
5d8de80a18 | |||
|
604c16ec90 | ||
|
a3e821fb16 | ||
|
890f50704f | ||
|
3416fdf50c | ||
43892f0d8c | |||
8e6b1e6f52 | |||
|
ff943a125c | ||
9cbbdff993 | |||
316358765d | |||
14737b5f12 | |||
caff27b43d | |||
589a94216b | |||
a659fb06fa | |||
f733fe5636 | |||
18423fcd9c | |||
72a3681767 | |||
160ccbb220 | |||
22f96363ba | |||
3e7f90733e | |||
321f038c7c | |||
7a9de79aae | |||
862830e9de | |||
bc0e536d3d | |||
82884a75a3 | |||
93c44861e0 | |||
4ecfc9de38 | |||
a84c3e0852 | |||
8efed966a0 | |||
e7c4b5bca7 | |||
ed632248c5 | |||
5c202741d6 | |||
c5ebdbf85b | |||
c4282a870e | |||
91cd6077ce | |||
c0ed6fa9c8 | |||
22c9c3e0f0 | |||
a709d24520 | |||
e56138e416 | |||
|
d516584d90 | ||
b420a9bba0 | |||
a20ab79355 | |||
e0ec15b272 | |||
9e998f9a29 | |||
1c7c0af712 | |||
d40a4a171f | |||
ba430a5849 | |||
0f21b01813 | |||
10586baf0d | |||
e979742424 | |||
ff3f049226 | |||
13b45c49da | |||
a72fb08dc8 | |||
944fa89b3c | |||
48e1b02e0e | |||
3e0bbe5ffe | |||
fd586d09c3 | |||
|
c2a4efde2b | ||
b615c27cf6 | |||
22e870b3e5 | |||
da462346f9 | |||
|
406bd84875 | ||
|
c9deca1180 | ||
419cd185fc | |||
7976e237ca | |||
19ec8f0817 | |||
ef3a3d6717 | |||
c2019267d1 | |||
0091fbcabb | |||
|
6d4f3109f8 | ||
|
35cf3a1cd1 | ||
|
1a782c1db9 | ||
|
3d2ca606ca | ||
aa7497573e | |||
54c1fe51ef | |||
7a86b2ad3b | |||
6b23c284e5 | |||
aab6660edd | |||
c46f41e25f | |||
00c0445eed | |||
c8c6b05b8a | |||
08ff1056d7 | |||
0f4a7e900f | |||
2d6c434bde | |||
f5d2644031 | |||
09b4fa2860 | |||
a748c1b6b2 | |||
a17cf78d29 | |||
9847433e0a | |||
7ce50398d1 | |||
e30bc177e9 | |||
b9ff467c0c | |||
1174500e3f | |||
07df450b3c | |||
b8d938d3aa | |||
d1e9340ea9 | |||
8925e26c4c | |||
9b89462797 | |||
51bad64a51 | |||
7880294c6f | |||
451b15fb6b | |||
d20700ca74 | |||
9cf1cbe425 | |||
212cd48249 | |||
7adc0c9c08 | |||
0a19a6c48b | |||
66b5843b0d | |||
df89fe69e1 | |||
9498242901 | |||
eab6dbd74c | |||
6159686a8e | |||
1d82bc2261 | |||
bfcdd79f33 | |||
d49b68597c | |||
1d9f4fc13e | |||
6fa0d8e2ff | |||
109a2a2020 | |||
bc6d8098f3 | |||
01e75e9217 | |||
f2b17880ba | |||
1d4f116079 | |||
44c52b6ed7 | |||
454f67b317 | |||
e1506a3dcf | |||
b22a4adec1 | |||
1ba90a8b78 | |||
5dd4f2a3fb | |||
b40ad78294 | |||
11bb194f26 | |||
c0ccdce0a9 | |||
7d16b710d8 | |||
c8fc32a4e7 | |||
baeb5e0ccb | |||
51bf2534b8 | |||
d3fae34290 | |||
1a77092ccb | |||
36846c2587 | |||
222d801ecc | |||
1a0b4deab3 | |||
cb13b09291 | |||
de5ecc5fe7 | |||
5773305785 | |||
7985b20200 | |||
c34906cb1f | |||
c10e1a6a1b | |||
fabcccfa60 | |||
f3e3badff6 | |||
9d2d01e3e2 | |||
93df6a6a08 | |||
8c13e44a15 | |||
345dfbaa21 | |||
381e581e8e | |||
94d2e8203f | |||
f9e51618c5 | |||
f97ad2a262 | |||
59b14301ad | |||
8b097a4395 | |||
fd2486b5cf | |||
f9f25d898b | |||
b6b3ef83a4 | |||
d7232a53ef | |||
047bf0e12e | |||
09f61eb64d | |||
abdb17640b | |||
9af7b775a7 | |||
97741b113b | |||
0bb444cd50 | |||
1d07d7fb1d | |||
a3e5aab6c4 | |||
9ebfb643aa | |||
0aad21a52c | |||
68d1628546 | |||
f4f95b3292 | |||
574807d254 | |||
256ed6491b | |||
7a02ffcf0c | |||
dcd8fce59a | |||
64a7b49218 | |||
1bcbb2079e | |||
e63468433e | |||
9c972ffa6b | |||
a11776e1b3 | |||
6ac9ac29d8 | |||
fc8915ea33 | |||
cc36ddcb6d | |||
f3388aef31 | |||
4e170b25e2 | |||
3e258a35fa | |||
f66609bbae | |||
ebdc7b200a | |||
29e1e912d7 | |||
308f3327d0 | |||
46b2b354fd | |||
7676cfd40c | |||
4e350aa25a | |||
584d02db29 | |||
859de0513a | |||
266d3d06c3 | |||
92ea3df4aa | |||
c46c31ea36 | |||
5937c6bf5c | |||
da91eed9d4 | |||
e2b28a31b2 | |||
88b50c5dd7 | |||
43a1208cce | |||
2b865a365b | |||
c77558689a | |||
a9641fd70d | |||
|
2c30ccc405 | ||
|
7189235bee | ||
|
0506a8c452 | ||
|
a237b615f2 | ||
|
dae12ac498 | ||
|
1d77ad98dc | ||
|
10b3083647 | ||
|
3a8a5f40ba | ||
|
14bc11f4bd | ||
|
e29a24b376 | ||
|
35a090ef42 | ||
2587882eae | |||
24d4b8478e | |||
476a69fe1b | |||
c52f8312e4 | |||
6dbe74d12b | |||
332074375a | |||
5e4a9e7567 | |||
cfe3e526df | |||
eb2c73dce1 | |||
7e448f739a | |||
0014693ba8 | |||
|
9a528cfd14 | ||
|
8cc46d2782 | ||
45945ae9c5 | |||
d7ea8fa651 | |||
e75aa2c1f7 | |||
63eed81d3d | |||
f4ca4454ea | |||
|
bb1a846cb3 | ||
2704069e74 | |||
7d1a04f089 | |||
53fa6f64ce | |||
7fa38340dd | |||
7aab3cff8c | |||
299534ccf1 | |||
b2655b7f11 | |||
5abe9b78cc | |||
12825ae08a | |||
ac0b4b2ed1 | |||
5f666382ab | |||
ce917d9e89 | |||
3ce25d0e14 | |||
8fe8ff540b | |||
0b4bf58107 | |||
|
4bf56c525f | ||
|
33bba73a65 | ||
074345fcf9 | |||
|
a17150962e | ||
|
3ae75b27a5 | ||
|
b2d180e8eb | ||
e6a369dcdd | |||
|
b9c56b04ce | ||
b8bbd88078 | |||
d25128287e | |||
c45fcda2f1 | |||
211cc1f775 | |||
981f7ca4f6 | |||
05ed236e03 | |||
bab3f673eb | |||
767a3cd64c | |||
c4078c5280 | |||
|
dc198215b1 | ||
|
a48170321c | ||
|
1e638238ed | ||
|
4e81e2d869 | ||
d434194021 | |||
c6ca3f6be4 | |||
8093eae61a | |||
833d52a558 | |||
8262e14198 | |||
a31cac4e50 | |||
8b399781e8 | |||
caafe495be | |||
66b34eaea4 | |||
ea4c6d9314 | |||
7c823e07ca | |||
41585f831c | |||
d93a26f9b0 | |||
|
ff26ccc545 | ||
|
72c688b885 | ||
|
14648fec6c | ||
cf523e30f8 | |||
888d3befe9 | |||
017691a40c | |||
dc418771a7 | |||
c2068db050 | |||
c42b1cd66b | |||
b404ae95fb | |||
adaa0c63ef | |||
843b0d1e7e | |||
|
c95b97cb14 | ||
|
dd764a2e24 | ||
|
cb997159f7 | ||
7241cdbfcb | |||
0480c02633 | |||
0a97f610a4 | |||
5a0563df94 | |||
7597b96dae | |||
f37483e2f0 | |||
d0ad6395b5 | |||
106983a394 | |||
91b85af11a | |||
534d756318 | |||
6998c30dd1 | |||
449f90c95b | |||
e96c874300 | |||
b35460d3c1 | |||
124049c924 | |||
5fd3eb3c29 | |||
d83962c0ba | |||
41da099933 | |||
c9bb93ede6 | |||
ca13d9155c | |||
e338ce0025 | |||
03c32fd265 | |||
727f54ee57 | |||
b97965193b | |||
aec0abb2b6 | |||
2c361e5b96 | |||
131f3bcf46 | |||
520f5abdcd | |||
5d294b663c | |||
26073c8000 | |||
e4c2f644f3 | |||
3de46cef5e | |||
0cc0bdf9f7 | |||
72d5c186dd | |||
dc782d87a8 | |||
ddae746b9d | |||
4e1b2d5ddb | |||
d8800a665d | |||
6c3ff0e9db | |||
3ef64271e7 | |||
2dee47404d | |||
3792bfdc1f | |||
a0e97cfe5b | |||
a3be98c514 | |||
2b34b3a75c | |||
b4454f7517 | |||
d9c660b8ea | |||
d113ea82fd | |||
a92c640cb7 | |||
728815f0c6 | |||
6b5d3f74d1 | |||
bee9ad5ff5 | |||
6e7b46666e | |||
383f074cae | |||
d678a639b8 | |||
453fea569a | |||
5c75e35fe0 | |||
cff50538fa | |||
69a4530269 | |||
db19e38717 | |||
c796bbdcfc | |||
2b34a0900c | |||
e15bac98a5 | |||
ee9a683eb0 | |||
9274e585bd | |||
57a49819f4 | |||
abc01b7cee | |||
d19c20a9d7 | |||
0c6144d262 | |||
ec4e44a27c | |||
69eb57f794 | |||
2347d64acc | |||
39c0560abe | |||
75459f852b | |||
6200728435 | |||
579b46db65 | |||
f99486c190 | |||
57a07af8ca | |||
5692440099 | |||
651969668f | |||
f3a028f1fc | |||
9f2d57ea03 | |||
d74a4cc33b | |||
308ca99fb0 | |||
08e709f1b3 | |||
9bc9d04a49 | |||
a553731f02 | |||
fc0043e3f6 | |||
d41ba79ae4 | |||
f36847d966 | |||
4011597d9c | |||
176473aa26 | |||
b41e52af63 | |||
fb32dde136 | |||
1e6cf6a33a | |||
2725323f16 | |||
5f1ee396d8 | |||
5f2c6cce16 | |||
3179ec1f62 | |||
b9a63f3c6f | |||
1d255372b2 | |||
306666a15e | |||
e05dbb4885 | |||
637e107525 | |||
59d0446020 | |||
f37ec13c75 | |||
0547bc7e5f | |||
02cb75f97a | |||
9c75942b0b | |||
5909659fa9 | |||
eaac771722 | |||
f3a5178de7 | |||
549d09890b | |||
4a9ef6b5f2 | |||
961d8d1a5c | |||
a904cdbf44 | |||
3af943f77c | |||
14455f65cb | |||
0c08654df3 | |||
acd0af25c6 | |||
66d2e2e838 | |||
4beeb9986a | |||
d81bc0eefb | |||
5b0a383513 | |||
367d3f0593 | |||
415c807464 | |||
9f562fe53e | |||
3bf2045f15 | |||
aced3e4fc3 | |||
fb0d2db156 | |||
a40fc37da4 | |||
f13f5996c1 | |||
00a2beae50 | |||
25bc7006a4 | |||
f643aa4d14 | |||
b8860478b6 | |||
847b91ca9e | |||
353e24de33 | |||
1c57c95d93 | |||
128a354b34 | |||
81c4189c8e | |||
b8e12e5215 | |||
113f1a5b34 | |||
4f04362835 | |||
fd8bfae8c7 | |||
eea3be3a39 | |||
849a950fbc | |||
6727bd3769 | |||
9f8fcf1ed0 | |||
4a599b902d | |||
a759f47c8e | |||
d181c34946 | |||
4968cdff37 | |||
bfa6eac4c2 | |||
1aeb18379c | |||
667a21d950 | |||
4416c008fc | |||
0a7389a71d | |||
383e64776d | |||
a27c1790b8 | |||
f81e3508ca | |||
127219f510 | |||
54cc33c819 | |||
a51f739d06 | |||
8d047ebe05 | |||
4e840ac17c | |||
f73b7a8b04 | |||
e986eaa538 | |||
d8db8df643 | |||
09eac03e10 | |||
51122d0fc5 | |||
d05c666513 | |||
a336856c9b | |||
0be570ae2d | |||
6a36bc43b5 | |||
1d27a88908 | |||
f378f15422 | |||
b2fb9faf6c | |||
859e203a00 | |||
6c6af5ec21 | |||
8ed98b3e6c | |||
4889e6d18b | |||
368c25125a | |||
283793a2ae | |||
bec7e5c69f | |||
d638aba85e | |||
df7788dd0b | |||
4a62773098 | |||
26d315b032 | |||
1927544533 | |||
c641a0c669 | |||
952da86931 | |||
be6391686f | |||
65794c1b20 | |||
|
967e53d83b | ||
b058f9d770 | |||
7df77a1343 | |||
f5dbfe553d | |||
7fe8f0b7d5 | |||
a9f9867976 | |||
b6d24bf929 | |||
e9684fcf45 | |||
a5b1c5b74e | |||
672eebb8fb | |||
8f834b3d76 | |||
6c93fb76b1 | |||
ea2df55295 | |||
7c08a0f0af | |||
9ad7d5a522 | |||
6c904a8b3f | |||
9ccb6cc066 | |||
2c98a8e133 | |||
4b6fd35e7a | |||
b3c7a3a337 | |||
7b374ad801 | |||
cb4a52e4f2 | |||
06ebcc0f07 | |||
e4ed9a65bb | |||
755899be4e | |||
4dede757d2 | |||
517f980664 | |||
31aea6b807 | |||
3a46fda769 | |||
c7bdfe90b7 | |||
|
d246cdee51 | ||
|
d518a76536 | ||
|
2a93ea7a0c | ||
|
f89b1acc6c | ||
ddeafe015b | |||
|
1dc8b054eb | ||
60134f14e9 | |||
4c33945081 | |||
a3ff3be5b1 | |||
ea52a7cf0a | |||
a993026380 | |||
|
6f65c33be4 | ||
|
4ee241714b | ||
|
d7dc743fa2 | ||
|
c220cbb767 | ||
0dc322729f | |||
726d6dd338 | |||
a8b202bd79 | |||
221db4e998 | |||
0e376e0d9e | |||
7aa44caea2 | |||
188cb573dd | |||
6dbd250694 | |||
cc760e7698 | |||
8648ea599c | |||
77e4c5d43a | |||
b15ba367c4 | |||
4db862bc97 | |||
6ad67f6adc | |||
0caf82a27e | |||
a430857309 | |||
2643c0c9ff | |||
b8852e1ab3 | |||
0dea34daab | |||
8057313c78 | |||
1bd96d0689 | |||
bbfe46f162 | |||
eff2cbde8b | |||
5c85ecffd1 | |||
|
ad1f9233ca | ||
e9fdd1ddbe | |||
8a03b05c90 | |||
|
c18c40b1b8 | ||
5a38b40eaf | |||
|
6112ca175f | ||
c7ebca4729 | |||
fa0070b9a8 | |||
4a447fbb45 | |||
cb32cfe9d8 | |||
cf4e80dcb8 | |||
|
c02d70eaa4 | ||
909d1f3409 | |||
66cf90f044 | |||
74d5da3987 | |||
f215159725 | |||
a6301163eb | |||
|
a07fce527f | ||
|
8a5209584d | ||
|
ddc8f6dad1 | ||
|
c2b68231f5 | ||
|
3f3dbf414c | ||
|
e4c3e9f015 | ||
|
9b71244391 | ||
|
ada1f26b68 | ||
|
935b777e57 | ||
|
7d3ff690f0 | ||
|
a455cfb854 | ||
|
19372c17f4 | ||
|
1cc2f58ab3 | ||
|
3756bdc98a | ||
|
e07f88b368 | ||
472624a831 | |||
|
84422684c5 | ||
|
28fc7a0462 | ||
|
78d349ccc7 | ||
|
0813a619b4 | ||
|
5ed9d2643b | ||
|
8dcba67fe9 | ||
|
5cac17676a | ||
4439cc249a | |||
e754773bce | |||
63c7c65a00 | |||
9925b3a2e5 | |||
a1056cf287 | |||
e6e701a609 | |||
b66891a646 | |||
7197382911 | |||
5c5f4cbc8e | |||
e581a1ad2f | |||
929e812992 | |||
3fe66466c5 | |||
f6979868e5 | |||
e8be64ae28 | |||
e4d8bfad7b | |||
96358959b4 | |||
bc9dfe4f65 | |||
ffa00ae15c | |||
13294b42d3 | |||
81159e77d1 | |||
5bd51b280e | |||
71e1ae6e3c | |||
10de039da0 | |||
27aa71e0ee | |||
a9a429824c | |||
4098ac8a19 | |||
2982f08b41 | |||
75ab9c66a3 | |||
95f02231b3 | |||
599f7a2857 | |||
2e7c07e6f4 | |||
01f7571185 | |||
065c8e5c1d | |||
dc14480519 | |||
aaf4923f64 | |||
e9c645bd87 | |||
1be6da9139 | |||
fa2930d93a | |||
a409e292ab | |||
00c86a2850 | |||
4daac7e90b | |||
06e23565df | |||
b19188165b | |||
2ed9f083bb | |||
e323ffa078 | |||
68de83af97 | |||
a3f2b23128 | |||
09f970db9b | |||
04edd90c21 | |||
a3f410875a | |||
3f3e98e637 | |||
660f8b7aa2 | |||
|
8ae3372c36 | ||
3a7c3ffc67 | |||
78c033e61c | |||
b5ef483f34 | |||
006eef0a28 | |||
e99a788c60 | |||
b5d33a98f0 | |||
f4f475bd45 | |||
6fe2e7287f | |||
1e9c9cf6ad | |||
3cb0b099e4 | |||
b53658e038 | |||
512460fdeb | |||
762f90adf6 | |||
|
9b4a4eeaf4 | ||
5e4e6a21a0 | |||
ffeb28e851 | |||
|
9c751aff30 | ||
|
b31d3b907a | ||
|
d49f866ca4 | ||
|
dde9f552a6 | ||
0f9b5551ec | |||
|
514e043e38 | ||
|
836e5fe8ee | ||
|
660fcaa0b6 | ||
44015b1c76 | |||
|
7438531900 | ||
|
681f293d3c | ||
|
8e3ff27bb8 | ||
|
dd2468a4d7 | ||
63cee42261 | |||
9f034967b5 | |||
ad48890a9f | |||
c0a3ad7e2b | |||
a3dffdf4e9 | |||
47d39a1c6f | |||
f5f1cbaaba | |||
134aa96194 | |||
0cdb23fbea | |||
ca655b0cdc | |||
da93444d3f | |||
c2564a9b8f | |||
33c6b35f8f | |||
ba72b3bf44 | |||
e961b6cb6a | |||
2c059afa7d | |||
9aab7d3a9b | |||
a25beb5b80 | |||
39d27209cd | |||
16a56ef29d | |||
6d02ea79ec | |||
deb4706b1d | |||
e14fd5e496 | |||
01c13ec581 | |||
dd97531861 | |||
be33b7458f | |||
4c3f63a48f | |||
d1d5c897d1 | |||
04ea11dd6d | |||
5722552395 | |||
421fbe5543 | |||
de24ca0648 | |||
175cbd1983 | |||
8175d6ac12 | |||
d4bae50ff0 | |||
d7376a4196 | |||
eb9f470ac5 | |||
7a12588744 | |||
6947b1e4a4 | |||
2cc85379cc | |||
d001c5a7fc | |||
de8c7ce34a | |||
|
cad8e86c8b | ||
|
0feb013c78 | ||
643b4d87a9 | |||
5abaf28f49 | |||
17804e7641 | |||
|
db3ec1991d | ||
|
62b7adb967 | ||
|
6fa5f49bc0 | ||
2bf7a5c246 | |||
98c74dffc4 | |||
771a1e8169 | |||
220c5b2081 | |||
2fc8a0e5a7 | |||
c6c66f956a | |||
1957606bc2 | |||
8ffc5d67cc | |||
2d7b6bd125 | |||
5105e902f1 | |||
19b2fa65fa | |||
ee4db50e00 | |||
99d18322b2 | |||
a07a8703f4 | |||
94f12edeaf | |||
fee51edade | |||
f90fde90fb | |||
ab6433168a | |||
0c360c0cc4 | |||
560714838f | |||
c365eaa1d1 | |||
337419c9f2 | |||
d4c4db09f3 | |||
3a3ddc9922 | |||
1c1ea74088 | |||
14c8230fd4 | |||
b6e4ba7ede | |||
2b511aba4c | |||
626fca1a55 | |||
0c96831378 | |||
fe4df78f80 | |||
31be1fa221 | |||
ba4d0abcda | |||
632dd860ff | |||
07c8638a6d | |||
c71f6e76d7 | |||
2559b4e604 | |||
460e6fa2c8 | |||
c3c5c83662 | |||
143c9b14a1 | |||
42ab2dd577 | |||
26959be149 | |||
118b5577bb | |||
4e69fa454d | |||
9fb3ef8e2a | |||
ec4201139a | |||
bcc73dec23 | |||
828f9a17fc | |||
de4aaeb02b | |||
cc323707fc | |||
ef0c207fc4 |
20
.env.example
Normal file
20
.env.example
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#Serve Http
|
||||||
|
PORT=7123
|
||||||
|
HOST=127.0.0.1
|
||||||
|
|
||||||
|
#AfricasTalking USSD POST endpoint
|
||||||
|
AT_ENDPOINT=/ussd/africastalking
|
||||||
|
|
||||||
|
#PostgreSQL
|
||||||
|
DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
|
||||||
|
#DB_TIMEZONE=Africa/Nairobi
|
||||||
|
#DB_SCHEMA=vise
|
||||||
|
|
||||||
|
#External API Calls
|
||||||
|
CUSTODIAL_URL_BASE=http://localhost:5003
|
||||||
|
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
|
||||||
|
DATA_URL_BASE=http://localhost:5006
|
||||||
|
|
||||||
|
#Language
|
||||||
|
DEFAULT_LANGUAGE=eng
|
||||||
|
LANGUAGES=eng, swa
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,3 +4,6 @@ go.work*
|
|||||||
**/*/*.bin
|
**/*/*.bin
|
||||||
**/*/.state/
|
**/*/.state/
|
||||||
cmd/.state/
|
cmd/.state/
|
||||||
|
id_*
|
||||||
|
*.gdbm
|
||||||
|
*.log
|
||||||
|
146
cmd/main.go
146
cmd/main.go
@ -1,146 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/cache"
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.defalsify.org/vise.git/persist"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.defalsify.org/vise.git/state"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
scriptDir = path.Join("services", "registration")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var dir string
|
|
||||||
var root string
|
|
||||||
var size uint
|
|
||||||
var sessionId string
|
|
||||||
flag.StringVar(&dir, "d", ".", "resource dir to read from")
|
|
||||||
flag.UintVar(&size, "s", 0, "max size of output")
|
|
||||||
flag.StringVar(&root, "root", "root", "entry point symbol")
|
|
||||||
flag.StringVar(&sessionId, "session-id", "default", "session id")
|
|
||||||
flag.Parse()
|
|
||||||
fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
st := state.NewState(16)
|
|
||||||
st.UseDebug()
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_LANGUAGE_SET, "LANGUAGE_CHANGE")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATED, "ACCOUNT_CREATED")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_SUCCESS, "ACCOUNT_SUCCESS")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_PENDING, "ACCOUNT_PENDING")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_INCORRECTPIN, "INCORRECTPIN")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_INCORRECTDATEFORMAT, "INVALIDDATEFORMAT")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT, "INVALIDRECIPIENT")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_PINMISMATCH, "PINMISMATCH")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_PIN_SET, "PIN_SET")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE, "INVALIDRECIPIENT_WITH_INVITE")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_INVALID_AMOUNT, "INVALIDAMOUNT")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_ALLOW_UPDATE, "UNLOCKFORUPDATE")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "VALIDPIN")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_VALIDPIN, "ACCOUNTUNLOCKED")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_ACCOUNT_CREATION_FAILED, "ACCOUNT_CREATION_FAILED")
|
|
||||||
state.FlagDebugger.Register(models.USERFLAG_SINGLE_EDIT, "SINGLEEDIT")
|
|
||||||
|
|
||||||
rfs := resource.NewFsResource(scriptDir)
|
|
||||||
ca := cache.NewCache()
|
|
||||||
cfg := engine.Config{
|
|
||||||
Root: "root",
|
|
||||||
SessionId: sessionId,
|
|
||||||
}
|
|
||||||
|
|
||||||
dp := path.Join(scriptDir, ".state")
|
|
||||||
err := os.MkdirAll(dp, 0700)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "state dir create exited with error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
pr := persist.NewFsPersister(dp)
|
|
||||||
en, err := engine.NewPersistedEngine(ctx, cfg, pr, rfs)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
pr = pr.WithContent(&st, ca)
|
|
||||||
err = pr.Save(cfg.SessionId)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to save state with error: %v\n", err)
|
|
||||||
}
|
|
||||||
en, err = engine.NewPersistedEngine(ctx, cfg, pr, rfs)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "engine create exited with error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fp := path.Join(dp, sessionId)
|
|
||||||
|
|
||||||
ussdHandlers := ussd.NewHandlers(fp, &st)
|
|
||||||
|
|
||||||
rfs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
|
|
||||||
rfs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
|
||||||
rfs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
|
|
||||||
rfs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
|
|
||||||
rfs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
|
|
||||||
rfs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
|
|
||||||
rfs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
|
|
||||||
rfs.AddLocalFunc("quit", ussdHandlers.Quit)
|
|
||||||
rfs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
|
|
||||||
rfs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
|
|
||||||
rfs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
|
|
||||||
rfs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
|
|
||||||
rfs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
|
|
||||||
rfs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
|
|
||||||
rfs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
|
|
||||||
rfs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
|
|
||||||
rfs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
|
|
||||||
rfs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
|
|
||||||
rfs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
|
|
||||||
rfs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
|
|
||||||
rfs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
|
|
||||||
rfs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
|
|
||||||
rfs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
|
|
||||||
rfs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
|
|
||||||
rfs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
|
|
||||||
rfs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
|
|
||||||
rfs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
|
|
||||||
rfs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
|
|
||||||
rfs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
|
|
||||||
rfs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
|
|
||||||
rfs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
|
|
||||||
rfs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
|
|
||||||
|
|
||||||
cont, err := en.Init(ctx)
|
|
||||||
en.SetDebugger(engine.NewSimpleDebug(nil))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if !cont {
|
|
||||||
_, err = en.WriteResult(ctx, os.Stdout)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "dead init write error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
err = en.Finish()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "engine finish error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
os.Stdout.Write([]byte{0x0a})
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
err = engine.Loop(ctx, en, os.Stdin, os.Stdout)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,59 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/env"
|
||||||
const (
|
|
||||||
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create"
|
|
||||||
TrackStatusURL = "https://custodial.sarafu.africa/api/track/"
|
|
||||||
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultLanguage = "eng"
|
||||||
|
languages []string
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DbConn string
|
||||||
|
DefaultLanguage string
|
||||||
|
Languages []string
|
||||||
|
)
|
||||||
|
|
||||||
|
func setLanguage() error {
|
||||||
|
defaultLanguage = env.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
|
||||||
|
languages = strings.Split(env.GetEnv("LANGUAGES", defaultLanguage), ",")
|
||||||
|
haveDefaultLanguage := false
|
||||||
|
for i, v := range(languages) {
|
||||||
|
languages[i] = strings.ReplaceAll(v, " ", "")
|
||||||
|
if languages[i] == defaultLanguage {
|
||||||
|
haveDefaultLanguage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !haveDefaultLanguage {
|
||||||
|
languages = append([]string{defaultLanguage}, languages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setConn() error {
|
||||||
|
DbConn = env.GetEnv("DB_CONN", "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig initializes the configuration values after environment variables are loaded.
|
||||||
|
func LoadConfig() error {
|
||||||
|
err := setConn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = setLanguage()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
DefaultLanguage = defaultLanguage
|
||||||
|
Languages = languages
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
28
doc/data.md
Normal file
28
doc/data.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Internals
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
This document describes component versions:
|
||||||
|
|
||||||
|
* `urdt-ussd` `v0.5.0-beta`
|
||||||
|
* `go-vise` `v0.2.2`
|
||||||
|
|
||||||
|
|
||||||
|
## User profile data
|
||||||
|
|
||||||
|
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
|
||||||
|
|
||||||
|
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
|
||||||
|
|
||||||
|
```
|
||||||
|
0x322b3235343132330002
|
||||||
|
|
||||||
|
prefix sessionid subprefix
|
||||||
|
32 2b323534313233 0002
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sub-prefixes
|
||||||
|
|
||||||
|
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
|
||||||
|
|
||||||
|
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.
|
13
entry/handlers.go
Normal file
13
entry/handlers.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package entry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.defalsify.org/vise.git/persist"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntryHandler interface {
|
||||||
|
Init(context.Context, string, []byte) (resource.Result, error) // HandlerFunc
|
||||||
|
Exit()
|
||||||
|
SetPersister(*persist.Persister)
|
||||||
|
}
|
40
env/load.go
vendored
Normal file
40
env/load.go
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadEnvVariables() {
|
||||||
|
LoadEnvVariablesPath(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadEnvVariablesPath(dir string) {
|
||||||
|
fp := path.Join(dir, ".env")
|
||||||
|
err := godotenv.Load(fp)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
15
errors/errors.go
Normal file
15
errors/errors.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidRequest = errors.New("invalid request for context")
|
||||||
|
ErrSessionMissing = errors.New("missing session")
|
||||||
|
ErrInvalidInput = errors.New("invalid input")
|
||||||
|
ErrStorage = errors.New("storage retrieval fail")
|
||||||
|
ErrEngineType = errors.New("incompatible engine")
|
||||||
|
ErrEngineInit = errors.New("engine init fail")
|
||||||
|
ErrEngineExec = errors.New("engine exec fail")
|
||||||
|
)
|
1
go-vise
1
go-vise
@ -1 +0,0 @@
|
|||||||
Subproject commit 1f47a674d95380be8c387f410f0342eb72357df5
|
|
40
go.mod
40
go.mod
@ -1,5 +1,39 @@
|
|||||||
module git.grassecon.net/urdt/ussd
|
module git.grassecon.net/grassrootseconomics/visedriver
|
||||||
|
|
||||||
go 1.22.6
|
go 1.23.0
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.9.0 // indirect
|
require (
|
||||||
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.2
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
|
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||||
|
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/peteole/testdata-loader v0.3.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
golang.org/x/crypto v0.27.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/repr v0.2.0 // indirect
|
||||||
|
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||||
|
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
golang.org/x/text v0.18.0 // indirect
|
||||||
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
74
go.sum
74
go.sum
@ -1,2 +1,76 @@
|
|||||||
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
|
||||||
|
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
|
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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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/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 v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
|
||||||
|
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0=
|
||||||
|
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
|
||||||
|
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
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/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/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U=
|
||||||
|
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/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/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
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/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||||
|
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||||
|
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 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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -1,779 +0,0 @@
|
|||||||
package ussd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
|
||||||
"git.defalsify.org/vise.git/lang"
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.defalsify.org/vise.git/state"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers/server"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/models"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
|
||||||
"gopkg.in/leonelquinteros/gotext.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
scriptDir = path.Join("services", "registration")
|
|
||||||
translationDir = path.Join(scriptDir, "locale")
|
|
||||||
)
|
|
||||||
|
|
||||||
type FSData struct {
|
|
||||||
Path string
|
|
||||||
St *state.State
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type Handlers struct {
|
|
||||||
fs *FSData
|
|
||||||
accountFileHandler utils.AccountFileHandlerInterface
|
|
||||||
accountService server.AccountServiceInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandlers(path string, st *state.State) *Handlers {
|
|
||||||
return &Handlers{
|
|
||||||
fs: &FSData{
|
|
||||||
Path: path,
|
|
||||||
St: st,
|
|
||||||
},
|
|
||||||
accountFileHandler: utils.NewAccountFileHandler(path + "_data"),
|
|
||||||
accountService: &server.AccountService{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Define the regex pattern as a constant
|
|
||||||
const pinPattern = `^\d{4}$`
|
|
||||||
|
|
||||||
// isValidPIN checks whether the given input is a 4 digit number
|
|
||||||
func isValidPIN(pin string) bool {
|
|
||||||
match, _ := regexp.MatchString(pinPattern, pin)
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLanguage sets the language across the menu
|
|
||||||
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
inputStr := string(input)
|
|
||||||
res := resource.Result{}
|
|
||||||
switch inputStr {
|
|
||||||
case "0":
|
|
||||||
res.FlagSet = []uint32{state.FLAG_LANG}
|
|
||||||
res.Content = "eng"
|
|
||||||
case "1":
|
|
||||||
res.FlagSet = []uint32{state.FLAG_LANG}
|
|
||||||
res.Content = "swa"
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_LANGUAGE_SET)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAccount checks if any account exists on the JSON data file, and if not
|
|
||||||
// creates an account on the API,
|
|
||||||
// sets the default values and flags
|
|
||||||
func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
err := h.accountFileHandler.EnsureFileExists()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if an account exists, return to prevent duplicate account creation
|
|
||||||
existingAccountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if existingAccountData != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accountResp, err := h.accountService.CreateAccount()
|
|
||||||
if err != nil {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_CREATION_FAILED)
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accountData := map[string]string{
|
|
||||||
"TrackingId": accountResp.Result.TrackingId,
|
|
||||||
"PublicKey": accountResp.Result.PublicKey,
|
|
||||||
"CustodialId": accountResp.Result.CustodialId.String(),
|
|
||||||
"Status": "PENDING",
|
|
||||||
"Gender": "Not provided",
|
|
||||||
"YOB": "Not provided",
|
|
||||||
"Location": "Not provided",
|
|
||||||
"Offerings": "Not provided",
|
|
||||||
"FirstName": "Not provided",
|
|
||||||
"FamilyName": "Not provided",
|
|
||||||
}
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_CREATED)
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SavePin persists the user's PIN choice into the filesystem
|
|
||||||
func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
accountPIN := string(input)
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that the PIN is a 4-digit number
|
|
||||||
if !isValidPIN(accountPIN) {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTPIN)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN)
|
|
||||||
accountData["AccountPIN"] = accountPIN
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information.
|
|
||||||
func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
menuOption := string(input)
|
|
||||||
switch menuOption {
|
|
||||||
case "2":
|
|
||||||
res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT)
|
|
||||||
case "3":
|
|
||||||
res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT)
|
|
||||||
case "4":
|
|
||||||
res.FlagReset = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_SINGLE_EDIT)
|
|
||||||
default:
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_SINGLE_EDIT)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyPin checks whether the confirmation PIN is similar to the account PIN
|
|
||||||
// If similar, it sets the USERFLAG_PIN_SET flag allowing the user
|
|
||||||
// to access the main menu
|
|
||||||
func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.Equal(input, []byte(accountData["AccountPIN"])) {
|
|
||||||
res.FlagSet = []uint32{models.USERFLAG_VALIDPIN}
|
|
||||||
res.FlagReset = []uint32{models.USERFLAG_PINMISMATCH}
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_PIN_SET)
|
|
||||||
} else {
|
|
||||||
res.FlagSet = []uint32{models.USERFLAG_PINMISMATCH}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//codeFromCtx retrieves language codes from the context that can be used for handling translations
|
|
||||||
func codeFromCtx(ctx context.Context) string {
|
|
||||||
var code string
|
|
||||||
engine.Logg.DebugCtxf(ctx, "in msg", "ctx", ctx, "val", code)
|
|
||||||
if ctx.Value("Language") != nil {
|
|
||||||
lang := ctx.Value("Language").(lang.Language)
|
|
||||||
code = lang.Code
|
|
||||||
}
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveFirstname updates the first name in a JSON data file with the provided input.
|
|
||||||
func (h *Handlers) SaveFirstname(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
if len(input) > 0 {
|
|
||||||
name := string(input)
|
|
||||||
accountData["FirstName"] = name
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveFamilyname updates the family name in a JSON data file with the provided input.
|
|
||||||
func (h *Handlers) SaveFamilyname(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
if len(input) > 0 {
|
|
||||||
secondname := string(input)
|
|
||||||
accountData["FamilyName"] = secondname
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveYOB updates the Year of Birth(YOB) in a JSON data file with the provided input.
|
|
||||||
func (h *Handlers) SaveYob(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
yob := string(input)
|
|
||||||
if len(yob) == 4 {
|
|
||||||
yob := string(input)
|
|
||||||
accountData["YOB"] = yob
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveLocation updates the location in a JSON data file with the provided input.
|
|
||||||
func (h *Handlers) SaveLocation(cxt context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(input) > 0 {
|
|
||||||
location := string(input)
|
|
||||||
accountData["Location"] = location
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveGender updates the gender in a JSON data file with the provided input.
|
|
||||||
func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(input) > 0 {
|
|
||||||
gender := string(input)
|
|
||||||
|
|
||||||
switch gender {
|
|
||||||
case "1":
|
|
||||||
gender = "Male"
|
|
||||||
case "2":
|
|
||||||
gender = "Female"
|
|
||||||
case "3":
|
|
||||||
gender = "Unspecified"
|
|
||||||
}
|
|
||||||
accountData["Gender"] = gender
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveOfferings updates the offerings(goods and services provided by the user) in a JSON data file with the provided input.
|
|
||||||
func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(input) > 0 {
|
|
||||||
offerings := string(input)
|
|
||||||
accountData["Offerings"] = offerings
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetAllowUpdate resets the allowupdate flag that allows a user to update profile data.
|
|
||||||
func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ALLOW_UPDATE)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
|
|
||||||
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckIdentifier retrieves the PublicKey from the JSON data file.
|
|
||||||
func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Content = accountData["PublicKey"]
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorize attempts to unlock the next sequential nodes by verifying the provided PIN against the already set PIN.
|
|
||||||
// It sets the required flags that control the flow.
|
|
||||||
func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
pin := string(input)
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(input) == 4 {
|
|
||||||
if pin != accountData["AccountPIN"] {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTPIN)
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
if h.fs.St.MatchFlag(models.USERFLAG_ACCOUNT_AUTHORIZED, false) {
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN)
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
} else {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_ALLOW_UPDATE)
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt.
|
|
||||||
func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTPIN)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckAccountStatus queries the API using the TrackingId and sets flags
|
|
||||||
// based on the account status
|
|
||||||
func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := h.accountService.CheckAccountStatus(accountData["TrackingId"])
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error checking account status:", err)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
accountData["Status"] = status
|
|
||||||
|
|
||||||
if status == "SUCCESS" {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_ACCOUNT_SUCCESS)
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_PENDING)
|
|
||||||
} else {
|
|
||||||
res.FlagReset = append(res.FlagSet, models.USERFLAG_ACCOUNT_SUCCESS)
|
|
||||||
res.FlagSet = append(res.FlagReset, models.USERFLAG_ACCOUNT_PENDING)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit displays the Thank you message and exits the menu
|
|
||||||
func (h *Handlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
code := codeFromCtx(ctx)
|
|
||||||
l := gotext.NewLocale(translationDir, code)
|
|
||||||
l.AddDomain("default")
|
|
||||||
|
|
||||||
res.Content = l.Get("Thank you for using Sarafu. Goodbye!")
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyYob verifies the length of the given input
|
|
||||||
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
date := string(input)
|
|
||||||
_, err := strconv.Atoi(date)
|
|
||||||
if err != nil {
|
|
||||||
// If conversion fails, input is not numeric
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTDATEFORMAT)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(date) == 4 {
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTDATEFORMAT)
|
|
||||||
} else {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INCORRECTDATEFORMAT)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetIncorrectYob resets the incorrect date format after a new attempt
|
|
||||||
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INCORRECTDATEFORMAT)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckBalance retrieves the balance from the API using the "PublicKey" and sets
|
|
||||||
// the balance as the result content
|
|
||||||
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balance, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
|
||||||
if err != nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
res.Content = balance
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateRecipient validates that the given input is a valid phone number.
|
|
||||||
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
recipient := string(input)
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if recipient != "0" {
|
|
||||||
// mimic invalid number check
|
|
||||||
if recipient == "000" {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_RECIPIENT)
|
|
||||||
res.Content = recipient
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
accountData["Recipient"] = recipient
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionReset resets the previous transaction data (Recipient and Amount)
|
|
||||||
// as well as the invalid flags
|
|
||||||
func (h *Handlers) TransactionReset(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset the transaction
|
|
||||||
accountData["Recipient"] = ""
|
|
||||||
accountData["Amount"] = ""
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INVALID_RECIPIENT, models.USERFLAG_INVALID_RECIPIENT_WITH_INVITE)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetTransactionAmount resets the transaction amount and invalid flag
|
|
||||||
func (h *Handlers) ResetTransactionAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset the amount
|
|
||||||
accountData["Amount"] = ""
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_INVALID_AMOUNT)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxAmount gets the current balance from the API and sets it as
|
|
||||||
// the result content.
|
|
||||||
func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balance, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
|
||||||
if err != nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Content = balance
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAmount ensures that the given input is a valid amount and that
|
|
||||||
// it is not more than the current balance.
|
|
||||||
func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
amountStr := string(input)
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balanceStr, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
res.Content = balanceStr
|
|
||||||
|
|
||||||
// Parse the balance
|
|
||||||
balanceParts := strings.Split(balanceStr, " ")
|
|
||||||
if len(balanceParts) != 2 {
|
|
||||||
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
|
|
||||||
}
|
|
||||||
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
|
|
||||||
if err != nil {
|
|
||||||
return res, fmt.Errorf("failed to parse balance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract numeric part from input
|
|
||||||
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
|
|
||||||
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
|
|
||||||
if len(matches) < 2 {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT)
|
|
||||||
res.Content = amountStr
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
inputAmount, err := strconv.ParseFloat(matches[1], 64)
|
|
||||||
if err != nil {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT)
|
|
||||||
res.Content = amountStr
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if inputAmount > balanceValue {
|
|
||||||
res.FlagSet = append(res.FlagSet, models.USERFLAG_INVALID_AMOUNT)
|
|
||||||
res.Content = amountStr
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
|
|
||||||
accountData["Amount"] = res.Content
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRecipient returns the transaction recipient from a JSON data file.
|
|
||||||
func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Content = accountData["Recipient"]
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProfileInfo retrieves and formats the profile information of a user from a JSON data file.
|
|
||||||
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
var age string
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
var name string
|
|
||||||
if accountData["FirstName"] == "Not provided" || accountData["FamilyName"] == "Not provided" {
|
|
||||||
name = "Not provided"
|
|
||||||
} else {
|
|
||||||
name = accountData["FirstName"] + " " + accountData["FamilyName"]
|
|
||||||
}
|
|
||||||
|
|
||||||
gender := accountData["Gender"]
|
|
||||||
yob := accountData["YOB"]
|
|
||||||
location := accountData["Location"]
|
|
||||||
offerings := accountData["Offerings"]
|
|
||||||
if yob == "Not provided" {
|
|
||||||
age = "Not provided"
|
|
||||||
} else {
|
|
||||||
ageInt, err := strconv.Atoi(yob)
|
|
||||||
if err != nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
age = strconv.Itoa(utils.CalculateAgeWithYOB(ageInt))
|
|
||||||
}
|
|
||||||
formattedData := fmt.Sprintf("Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", name, gender, age, location, offerings)
|
|
||||||
res.Content = formattedData
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSender retrieves the public key from a JSON data file.
|
|
||||||
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Content = accountData["PublicKey"]
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAmount retrieves the amount from a JSON data file.
|
|
||||||
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Content = accountData["Amount"]
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuickWithBalance retrieves the balance for a given public key from the custodial balance API endpoint before
|
|
||||||
// gracefully exiting the session.
|
|
||||||
func (h *Handlers) QuitWithBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
code := codeFromCtx(ctx)
|
|
||||||
l := gotext.NewLocale(translationDir, code)
|
|
||||||
l.AddDomain("default")
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
balance, err := h.accountService.CheckBalance(accountData["PublicKey"])
|
|
||||||
if err != nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
res.Content = l.Get("Your account balance is %s", balance)
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitiateTransaction returns a confirmation and resets the transaction data
|
|
||||||
// on the JSON file.
|
|
||||||
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
|
||||||
res := resource.Result{}
|
|
||||||
code := codeFromCtx(ctx)
|
|
||||||
l := gotext.NewLocale(translationDir, code)
|
|
||||||
l.AddDomain("default")
|
|
||||||
|
|
||||||
accountData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Use the amount, recipient and sender to call the API and initialize the transaction
|
|
||||||
|
|
||||||
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", accountData["Recipient"], accountData["Amount"], accountData["PublicKey"])
|
|
||||||
|
|
||||||
// reset the transaction
|
|
||||||
accountData["Recipient"] = ""
|
|
||||||
accountData["Amount"] = ""
|
|
||||||
|
|
||||||
err = h.accountFileHandler.WriteAccountData(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.FlagReset = append(res.FlagReset, models.USERFLAG_ACCOUNT_AUTHORIZED)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
@ -1,878 +0,0 @@
|
|||||||
package ussd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/resource"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers/ussd/mocks"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/models"
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
|
||||||
"github.com/alecthomas/assert/v2"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockAccountService implements AccountServiceInterface for testing
|
|
||||||
type MockAccountService struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Get(0).(*models.AccountResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
|
|
||||||
args := m.Called(publicKey)
|
|
||||||
return args.String(0), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
|
|
||||||
args := m.Called(trackingId)
|
|
||||||
return args.String(0), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateAccount(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
tempDir, err := os.MkdirTemp("", "test_create_account")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp directory: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir) // Clean up after the test run
|
|
||||||
|
|
||||||
sessionID := "07xxxxxxxx"
|
|
||||||
|
|
||||||
// Set up the data file path using the session ID
|
|
||||||
accountFilePath := filepath.Join(tempDir, sessionID+"_data")
|
|
||||||
|
|
||||||
// Initialize account file handler
|
|
||||||
accountFileHandler := utils.NewAccountFileHandler(accountFilePath)
|
|
||||||
|
|
||||||
// Create a mock account service
|
|
||||||
mockAccountService := &MockAccountService{}
|
|
||||||
mockAccountResponse := &models.AccountResponse{
|
|
||||||
Ok: true,
|
|
||||||
Result: struct {
|
|
||||||
CustodialId json.Number `json:"custodialId"`
|
|
||||||
PublicKey string `json:"publicKey"`
|
|
||||||
TrackingId string `json:"trackingId"`
|
|
||||||
}{
|
|
||||||
CustodialId: "test-custodial-id",
|
|
||||||
PublicKey: "test-public-key",
|
|
||||||
TrackingId: "test-tracking-id",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up expectations for the mock account service
|
|
||||||
mockAccountService.On("CreateAccount").Return(mockAccountResponse, nil)
|
|
||||||
|
|
||||||
// Initialize Handlers with mock account service
|
|
||||||
h := &Handlers{
|
|
||||||
fs: &FSData{Path: accountFilePath},
|
|
||||||
accountFileHandler: accountFileHandler,
|
|
||||||
accountService: mockAccountService,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
existingData map[string]string
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedData map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "New account creation",
|
|
||||||
existingData: nil,
|
|
||||||
expectedResult: resource.Result{
|
|
||||||
FlagSet: []uint32{models.USERFLAG_ACCOUNT_CREATED},
|
|
||||||
},
|
|
||||||
expectedData: map[string]string{
|
|
||||||
"TrackingId": "test-tracking-id",
|
|
||||||
"PublicKey": "test-public-key",
|
|
||||||
"CustodialId": "test-custodial-id",
|
|
||||||
"Status": "PENDING",
|
|
||||||
"Gender": "Not provided",
|
|
||||||
"YOB": "Not provided",
|
|
||||||
"Location": "Not provided",
|
|
||||||
"Offerings": "Not provided",
|
|
||||||
"FirstName": "Not provided",
|
|
||||||
"FamilyName": "Not provided",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Existing account",
|
|
||||||
existingData: map[string]string{
|
|
||||||
"TrackingId": "test-tracking-id",
|
|
||||||
"PublicKey": "test-public-key",
|
|
||||||
"CustodialId": "test-custodial-id",
|
|
||||||
"Status": "PENDING",
|
|
||||||
"Gender": "Not provided",
|
|
||||||
"YOB": "Not provided",
|
|
||||||
"Location": "Not provided",
|
|
||||||
"Offerings": "Not provided",
|
|
||||||
"FirstName": "Not provided",
|
|
||||||
"FamilyName": "Not provided",
|
|
||||||
},
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedData: map[string]string{
|
|
||||||
"TrackingId": "test-tracking-id",
|
|
||||||
"PublicKey": "test-public-key",
|
|
||||||
"CustodialId": "test-custodial-id",
|
|
||||||
"Status": "PENDING",
|
|
||||||
"Gender": "Not provided",
|
|
||||||
"YOB": "Not provided",
|
|
||||||
"Location": "Not provided",
|
|
||||||
"Offerings": "Not provided",
|
|
||||||
"FirstName": "Not provided",
|
|
||||||
"FamilyName": "Not provided",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the data file path using the session ID
|
|
||||||
accountFilePath := filepath.Join(tempDir, sessionID+"_data")
|
|
||||||
|
|
||||||
// Setup existing data if any
|
|
||||||
if tt.existingData != nil {
|
|
||||||
data, _ := json.Marshal(tt.existingData)
|
|
||||||
err := os.WriteFile(accountFilePath, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write existing data: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the function
|
|
||||||
result, err := h.CreateAccount(context.Background(), "", nil)
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateAccount returned an error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the result
|
|
||||||
if len(result.FlagSet) != len(tt.expectedResult.FlagSet) {
|
|
||||||
t.Errorf("Expected %d flags, got %d", len(tt.expectedResult.FlagSet), len(result.FlagSet))
|
|
||||||
}
|
|
||||||
for i, flag := range tt.expectedResult.FlagSet {
|
|
||||||
if result.FlagSet[i] != flag {
|
|
||||||
t.Errorf("Expected flag %d, got %d", flag, result.FlagSet[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the stored data
|
|
||||||
data, err := os.ReadFile(accountFilePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read account data file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var storedData map[string]string
|
|
||||||
err = json.Unmarshal(data, &storedData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal stored data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, expectedValue := range tt.expectedData {
|
|
||||||
if storedValue, ok := storedData[key]; !ok || storedValue != expectedValue {
|
|
||||||
t.Errorf("Expected %s to be %s, got %s", key, expectedValue, storedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateAccount_Success(t *testing.T) {
|
|
||||||
mockAccountFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
mockCreateAccountService := new(mocks.MockAccountService)
|
|
||||||
|
|
||||||
mockAccountFileHandler.On("EnsureFileExists").Return(nil)
|
|
||||||
|
|
||||||
// Mock that no account data exists
|
|
||||||
mockAccountFileHandler.On("ReadAccountData").Return(nil, nil)
|
|
||||||
|
|
||||||
// Define expected account response after api call
|
|
||||||
expectedAccountResp := &models.AccountResponse{
|
|
||||||
Ok: true,
|
|
||||||
Result: struct {
|
|
||||||
CustodialId json.Number `json:"custodialId"`
|
|
||||||
PublicKey string `json:"publicKey"`
|
|
||||||
TrackingId string `json:"trackingId"`
|
|
||||||
}{
|
|
||||||
CustodialId: "12",
|
|
||||||
PublicKey: "0x8E0XSCSVA",
|
|
||||||
TrackingId: "d95a7e83-196c-4fd0-866fSGAGA",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil)
|
|
||||||
|
|
||||||
// Mock WriteAccountData to not error
|
|
||||||
mockAccountFileHandler.On("WriteAccountData", mock.Anything).Return(nil)
|
|
||||||
|
|
||||||
handlers := &Handlers{
|
|
||||||
accountService: mockCreateAccountService,
|
|
||||||
}
|
|
||||||
|
|
||||||
actualResponse, err := handlers.accountService.CreateAccount()
|
|
||||||
|
|
||||||
// Assert results
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expectedAccountResp.Ok, true)
|
|
||||||
assert.Equal(t, expectedAccountResp, actualResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSavePin(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
tempDir, err := os.MkdirTemp("", "test_save_pin")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create temp directory: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
sessionID := "07xxxxxxxx"
|
|
||||||
|
|
||||||
// Set up the data file path using the session ID
|
|
||||||
accountFilePath := filepath.Join(tempDir, sessionID+"_data")
|
|
||||||
initialAccountData := map[string]string{
|
|
||||||
"TrackingId": "test-tracking-id",
|
|
||||||
"PublicKey": "test-public-key",
|
|
||||||
}
|
|
||||||
data, _ := json.Marshal(initialAccountData)
|
|
||||||
err = os.WriteFile(accountFilePath, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write initial account data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new AccountFileHandler and set it in the Handlers struct
|
|
||||||
accountFileHandler := utils.NewAccountFileHandler(accountFilePath)
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: accountFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
expectedFlags []uint32
|
|
||||||
expectedData map[string]string
|
|
||||||
expectedErrors bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid PIN",
|
|
||||||
input: []byte("1234"),
|
|
||||||
expectedFlags: []uint32{},
|
|
||||||
expectedData: map[string]string{
|
|
||||||
"TrackingId": "test-tracking-id",
|
|
||||||
"PublicKey": "test-public-key",
|
|
||||||
"AccountPIN": "1234",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN - non-numeric",
|
|
||||||
input: []byte("12ab"),
|
|
||||||
expectedFlags: []uint32{models.USERFLAG_INCORRECTPIN},
|
|
||||||
expectedData: initialAccountData, // No changes expected
|
|
||||||
expectedErrors: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN - less than 4 digits",
|
|
||||||
input: []byte("123"),
|
|
||||||
expectedFlags: []uint32{models.USERFLAG_INCORRECTPIN},
|
|
||||||
expectedData: initialAccountData, // No changes expected
|
|
||||||
expectedErrors: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN - more than 4 digits",
|
|
||||||
input: []byte("12345"),
|
|
||||||
expectedFlags: []uint32{models.USERFLAG_INCORRECTPIN},
|
|
||||||
expectedData: initialAccountData, // No changes expected
|
|
||||||
expectedErrors: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Ensure the file exists before running the test
|
|
||||||
err := accountFileHandler.EnsureFileExists()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to ensure account file exists: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := h.SavePin(context.Background(), "", tt.input)
|
|
||||||
if err != nil && !tt.expectedErrors {
|
|
||||||
t.Fatalf("SavePin returned an unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result.FlagSet) != len(tt.expectedFlags) {
|
|
||||||
t.Errorf("Expected %d flags, got %d", len(tt.expectedFlags), len(result.FlagSet))
|
|
||||||
}
|
|
||||||
for i, flag := range tt.expectedFlags {
|
|
||||||
if result.FlagSet[i] != flag {
|
|
||||||
t.Errorf("Expected flag %d, got %d", flag, result.FlagSet[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(accountFilePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read account data file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var storedData map[string]string
|
|
||||||
err = json.Unmarshal(data, &storedData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal stored data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, expectedValue := range tt.expectedData {
|
|
||||||
if storedValue, ok := storedData[key]; !ok || storedValue != expectedValue {
|
|
||||||
t.Errorf("Expected %s to be %s, got %s", key, expectedValue, storedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveLocation(t *testing.T) {
|
|
||||||
// Create a new instance of MockAccountFileHandler
|
|
||||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
|
|
||||||
// Define test cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
existingData map[string]string
|
|
||||||
writeError error
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful Save",
|
|
||||||
input: []byte("Mombasa"),
|
|
||||||
existingData: map[string]string{"Location": "Mombasa"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty location input",
|
|
||||||
input: []byte{},
|
|
||||||
existingData: map[string]string{"OtherKey": "OtherValue"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the mock expectations
|
|
||||||
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
|
||||||
if tt.expectedError == nil && len(tt.input) > 0 {
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["Location"] == string(tt.input)
|
|
||||||
})).Return(tt.writeError)
|
|
||||||
} else if len(tt.input) == 0 {
|
|
||||||
// For empty input, no WriteAccountData call should be made
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Handlers instance with the mock file handler
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Save Location
|
|
||||||
result, err := h.SaveLocation(context.Background(), "save_location", tt.input)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save location with error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
savedData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err == nil {
|
|
||||||
//Assert that the input provided is what was saved into the file
|
|
||||||
assert.Equal(t, string(tt.input), savedData["Location"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the results
|
|
||||||
assert.Equal(t, tt.expectedResult, result)
|
|
||||||
assert.Equal(t, tt.expectedError, err)
|
|
||||||
|
|
||||||
// Assert all expectations were met
|
|
||||||
mockFileHandler.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveFirstname(t *testing.T) {
|
|
||||||
// Create a new instance of MockAccountFileHandler
|
|
||||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
|
|
||||||
// Define test cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
existingData map[string]string
|
|
||||||
writeError error
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful Save",
|
|
||||||
input: []byte("Joe"),
|
|
||||||
existingData: map[string]string{"Name": "Joe"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Input",
|
|
||||||
input: []byte{},
|
|
||||||
existingData: map[string]string{"OtherKey": "OtherValue"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the mock expectations
|
|
||||||
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
|
||||||
if tt.expectedError == nil && len(tt.input) > 0 {
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["FirstName"] == string(tt.input)
|
|
||||||
})).Return(tt.writeError)
|
|
||||||
} else if len(tt.input) == 0 {
|
|
||||||
// For empty input, no WriteAccountData call should be made
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Handlers instance with the mock file handler
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call save location
|
|
||||||
result, err := h.SaveFirstname(context.Background(), "save_location", tt.input)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save first name with error: %v", err)
|
|
||||||
}
|
|
||||||
savedData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err == nil {
|
|
||||||
//Assert that the input provided is what was saved into the file
|
|
||||||
assert.Equal(t, string(tt.input), savedData["FirstName"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the results
|
|
||||||
assert.Equal(t, tt.expectedResult, result)
|
|
||||||
assert.Equal(t, tt.expectedError, err)
|
|
||||||
|
|
||||||
// Assert all expectations were met
|
|
||||||
mockFileHandler.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveFamilyName(t *testing.T) {
|
|
||||||
// Create a new instance of MockAccountFileHandler
|
|
||||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
|
|
||||||
// Define test cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
existingData map[string]string
|
|
||||||
writeError error
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful Save",
|
|
||||||
input: []byte("Doe"),
|
|
||||||
existingData: map[string]string{"FamilyName": "Doe"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Input",
|
|
||||||
input: []byte{},
|
|
||||||
existingData: map[string]string{"FamilyName": "Doe"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the mock expectations
|
|
||||||
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
|
||||||
if tt.expectedError == nil && len(tt.input) > 0 {
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["FamilyName"] == string(tt.input)
|
|
||||||
})).Return(tt.writeError)
|
|
||||||
} else if len(tt.input) == 0 {
|
|
||||||
// For empty input, no WriteAccountData call should be made
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Handlers instance with the mock file handler
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call save familyname
|
|
||||||
result, err := h.SaveFamilyname(context.Background(), "save_familyname", tt.input)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save family name with error: %v", err)
|
|
||||||
}
|
|
||||||
savedData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err == nil {
|
|
||||||
//Assert that the input provided is what was saved into the file
|
|
||||||
assert.Equal(t, string(tt.input), savedData["FamilyName"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the results
|
|
||||||
assert.Equal(t, tt.expectedResult, result)
|
|
||||||
assert.Equal(t, tt.expectedError, err)
|
|
||||||
|
|
||||||
// Assert all expectations were met
|
|
||||||
mockFileHandler.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveYOB(t *testing.T) {
|
|
||||||
// Create a new instance of MockAccountFileHandler
|
|
||||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
|
|
||||||
// Define test cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
existingData map[string]string
|
|
||||||
writeError error
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful Save",
|
|
||||||
input: []byte("2006"),
|
|
||||||
existingData: map[string]string{"": ""},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "YOB less than 4 digits(invalid date entry)",
|
|
||||||
input: []byte{},
|
|
||||||
existingData: map[string]string{"": ""},
|
|
||||||
writeError: nil,
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the mock expectations
|
|
||||||
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
|
||||||
if tt.expectedError == nil && len(tt.input) > 0 {
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["YOB"] == string(tt.input)
|
|
||||||
})).Return(tt.writeError)
|
|
||||||
} else if len(tt.input) != 4 {
|
|
||||||
// For input whose input is not a valid yob, no WriteAccountData call should be made
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Handlers instance with the mock file handler
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call save yob
|
|
||||||
result, err := h.SaveYob(context.Background(), "save_yob", tt.input)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save family name with error: %v", err)
|
|
||||||
}
|
|
||||||
savedData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err == nil {
|
|
||||||
//Assert that the input provided is what was saved into the file
|
|
||||||
assert.Equal(t, string(tt.input), savedData["YOB"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the results
|
|
||||||
assert.Equal(t, tt.expectedResult, result)
|
|
||||||
assert.Equal(t, tt.expectedError, err)
|
|
||||||
|
|
||||||
// Assert all expectations were met
|
|
||||||
mockFileHandler.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveOfferings(t *testing.T) {
|
|
||||||
// Create a new instance of MockAccountFileHandler
|
|
||||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
|
|
||||||
// Define test cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
existingData map[string]string
|
|
||||||
writeError error
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful Save",
|
|
||||||
input: []byte("Bananas"),
|
|
||||||
existingData: map[string]string{"": ""},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty input",
|
|
||||||
input: []byte{},
|
|
||||||
existingData: map[string]string{"": ""},
|
|
||||||
writeError: nil,
|
|
||||||
expectedError: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the mock expectations
|
|
||||||
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
|
||||||
if tt.expectedError == nil && len(tt.input) > 0 {
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["Offerings"] == string(tt.input)
|
|
||||||
})).Return(tt.writeError)
|
|
||||||
} else if len(tt.input) != 4 {
|
|
||||||
// For input whose input is not a valid yob, no WriteAccountData call should be made
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Handlers instance with the mock file handler
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call save yob
|
|
||||||
result, err := h.SaveOfferings(context.Background(), "save_offerings", tt.input)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save offerings with error: %v", err)
|
|
||||||
}
|
|
||||||
savedData, err := h.accountFileHandler.ReadAccountData()
|
|
||||||
if err == nil {
|
|
||||||
//Assert that the input provided is what was saved into the file
|
|
||||||
assert.Equal(t, string(tt.input), savedData["Offerings"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the results
|
|
||||||
assert.Equal(t, tt.expectedResult, result)
|
|
||||||
assert.Equal(t, tt.expectedError, err)
|
|
||||||
|
|
||||||
// Assert all expectations were met
|
|
||||||
mockFileHandler.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func TestSaveGender(t *testing.T) {
|
|
||||||
// Create a new instance of MockAccountFileHandler
|
|
||||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
|
|
||||||
// Define test cases
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []byte
|
|
||||||
existingData map[string]string
|
|
||||||
writeError error
|
|
||||||
expectedResult resource.Result
|
|
||||||
expectedError error
|
|
||||||
expectedGender string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Successful Save - Male",
|
|
||||||
input: []byte("1"),
|
|
||||||
existingData: map[string]string{"OtherKey": "OtherValue"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
expectedGender: "Male",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Successful Save - Female",
|
|
||||||
input: []byte("2"),
|
|
||||||
existingData: map[string]string{"OtherKey": "OtherValue"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
expectedGender: "Female",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Successful Save - Unspecified",
|
|
||||||
input: []byte("3"),
|
|
||||||
existingData: map[string]string{"OtherKey": "OtherValue"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
expectedGender: "Unspecified",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "Empty Input",
|
|
||||||
input: []byte{},
|
|
||||||
existingData: map[string]string{"OtherKey": "OtherValue"},
|
|
||||||
writeError: nil,
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
expectedError: nil,
|
|
||||||
expectedGender: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up the mock expectations
|
|
||||||
mockFileHandler.On("ReadAccountData").Return(tt.existingData, tt.expectedError)
|
|
||||||
if tt.expectedError == nil && len(tt.input) > 0 {
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["Gender"] == tt.expectedGender
|
|
||||||
})).Return(tt.writeError)
|
|
||||||
} else if len(tt.input) == 0 {
|
|
||||||
// For empty input, no WriteAccountData call should be made
|
|
||||||
mockFileHandler.On("WriteAccountData", mock.Anything).Maybe().Return(tt.writeError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Handlers instance with the mock file handler
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the method
|
|
||||||
result, err := h.SaveGender(context.Background(), "save_gender", tt.input)
|
|
||||||
|
|
||||||
// Assert the results
|
|
||||||
assert.Equal(t, tt.expectedResult, result)
|
|
||||||
assert.Equal(t, tt.expectedError, err)
|
|
||||||
|
|
||||||
// Verify WriteAccountData was called with the expected data
|
|
||||||
if len(tt.input) > 0 && tt.expectedError == nil {
|
|
||||||
mockFileHandler.AssertCalled(t, "WriteAccountData", mock.MatchedBy(func(data map[string]string) bool {
|
|
||||||
return data["Gender"] == tt.expectedGender
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert all expectations were met
|
|
||||||
mockFileHandler.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSender(t *testing.T) {
|
|
||||||
mockAccountFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockAccountFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
expectedResult resource.Result
|
|
||||||
accountData map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid public key",
|
|
||||||
expectedResult: resource.Result{
|
|
||||||
Content: "test-public-key",
|
|
||||||
},
|
|
||||||
accountData: map[string]string{
|
|
||||||
"PublicKey": "test-public-key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Missing public key",
|
|
||||||
expectedResult: resource.Result{
|
|
||||||
Content: "",
|
|
||||||
},
|
|
||||||
accountData: map[string]string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset the mock state
|
|
||||||
mockAccountFileHandler.Mock = mock.Mock{}
|
|
||||||
|
|
||||||
mockAccountFileHandler.On("ReadAccountData").Return(tt.accountData, nil)
|
|
||||||
|
|
||||||
result, err := h.GetSender(context.Background(), "", nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error occurred: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedResult.Content, result.Content)
|
|
||||||
mockAccountFileHandler.AssertCalled(t, "ReadAccountData")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAmount(t *testing.T) {
|
|
||||||
mockAccountFileHandler := new(mocks.MockAccountFileHandler)
|
|
||||||
h := &Handlers{
|
|
||||||
accountFileHandler: mockAccountFileHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
expectedResult resource.Result
|
|
||||||
accountData map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid amount",
|
|
||||||
expectedResult: resource.Result{
|
|
||||||
Content: "0.003",
|
|
||||||
},
|
|
||||||
accountData: map[string]string{
|
|
||||||
"Amount": "0.003",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Missing amount",
|
|
||||||
expectedResult: resource.Result{},
|
|
||||||
accountData: map[string]string{
|
|
||||||
"Amount": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset the mock state
|
|
||||||
mockAccountFileHandler.Mock = mock.Mock{}
|
|
||||||
|
|
||||||
mockAccountFileHandler.On("ReadAccountData").Return(tt.accountData, nil)
|
|
||||||
|
|
||||||
result, err := h.GetAmount(context.Background(), "", nil)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expectedResult.Content, result.Content)
|
|
||||||
|
|
||||||
mockAccountFileHandler.AssertCalled(t, "ReadAccountData")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
|||||||
package mocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/models"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MockAccountFileHandler struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountFileHandler) EnsureFileExists() error {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountFileHandler) ReadAccountData() (map[string]string, error) {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Get(0).(map[string]string), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountFileHandler) WriteAccountData(data map[string]string) error {
|
|
||||||
args := m.Called(data)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockAccountService struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Get(0).(*models.AccountResponse), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CheckAccountStatus(TrackingId string) (string, error) {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Get(0).(string), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAccountService) CheckBalance(PublicKey string) (string, error) {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Get(0).(string), args.Error(1)
|
|
||||||
}
|
|
@ -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"`
|
|
||||||
}
|
|
@ -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"`
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import "git.defalsify.org/vise.git/state"
|
|
||||||
|
|
||||||
const (
|
|
||||||
USERFLAG_LANGUAGE_SET = iota + state.FLAG_USERSTART
|
|
||||||
USERFLAG_ACCOUNT_CREATED
|
|
||||||
USERFLAG_ACCOUNT_PENDING
|
|
||||||
USERFLAG_ACCOUNT_SUCCESS
|
|
||||||
USERFLAG_ACCOUNT_AUTHORIZED
|
|
||||||
USERFLAG_INVALID_RECIPIENT
|
|
||||||
USERFLAG_INVALID_RECIPIENT_WITH_INVITE
|
|
||||||
USERFLAG_INCORRECTPIN
|
|
||||||
USERFLAG_ALLOW_UPDATE
|
|
||||||
USERFLAG_INVALID_AMOUNT
|
|
||||||
USERFLAG_PIN_SET
|
|
||||||
USERFLAG_VALIDPIN
|
|
||||||
USERFLAG_PINMISMATCH
|
|
||||||
USERFLAG_INCORRECTDATEFORMAT
|
|
||||||
USERFLAG_ACCOUNT_CREATION_FAILED
|
|
||||||
USERFLAG_SINGLE_EDIT
|
|
||||||
)
|
|
@ -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"`
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountFileHandler struct {
|
|
||||||
FilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAccountFileHandler(path string) *AccountFileHandler {
|
|
||||||
return &AccountFileHandler{FilePath: path}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (afh *AccountFileHandler) ReadAccountData() (map[string]string, error) {
|
|
||||||
jsonData, err := os.ReadFile(afh.FilePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountData map[string]string
|
|
||||||
err = json.Unmarshal(jsonData, &accountData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return accountData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (afh *AccountFileHandler) WriteAccountData(accountData map[string]string) error {
|
|
||||||
jsonData, err := json.Marshal(accountData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(afh.FilePath, jsonData, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (afh *AccountFileHandler) EnsureFileExists() error {
|
|
||||||
f, err := os.OpenFile(afh.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return f.Close()
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
|
|
||||||
// It adjusts for cases where the current date is before the birthday in the current year.
|
|
||||||
func CalculateAge(birthdate, today time.Time) int {
|
|
||||||
today = today.In(birthdate.Location())
|
|
||||||
ty, tm, td := today.Date()
|
|
||||||
today = time.Date(ty, tm, td, 0, 0, 0, 0, time.UTC)
|
|
||||||
by, bm, bd := birthdate.Date()
|
|
||||||
birthdate = time.Date(by, bm, bd, 0, 0, 0, 0, time.UTC)
|
|
||||||
if today.Before(birthdate) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
age := ty - by
|
|
||||||
anniversary := birthdate.AddDate(age, 0, 0)
|
|
||||||
if anniversary.After(today) {
|
|
||||||
age--
|
|
||||||
}
|
|
||||||
return age
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateAgeWithYOB calculates the age based on the given year of birth (YOB).
|
|
||||||
// It subtracts the YOB from the current year to determine the age.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// yob: The year of birth as an integer.
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// The calculated age as an integer.
|
|
||||||
func CalculateAgeWithYOB(yob int) int {
|
|
||||||
currentYear := time.Now().Year()
|
|
||||||
return currentYear - yob
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type AccountFileHandlerInterface interface {
|
|
||||||
EnsureFileExists() error
|
|
||||||
ReadAccountData() (map[string]string, error)
|
|
||||||
WriteAccountData(data map[string]string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
110
request/base.go
Normal file
110
request/base.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/grassrootseconomics/visedriver/storage"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/errors"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseRequestHandler struct {
|
||||||
|
cfgTemplate engine.Config
|
||||||
|
rp RequestParser
|
||||||
|
rs resource.Resource
|
||||||
|
hn entry.EntryHandler
|
||||||
|
provider storage.StorageProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
//func NewBaseRequestHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp request.RequestParser, hn *handlers.Handlers) *BaseRequestHandler {
|
||||||
|
func NewBaseRequestHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn entry.EntryHandler) *BaseRequestHandler {
|
||||||
|
return &BaseRequestHandler{
|
||||||
|
cfgTemplate: cfg,
|
||||||
|
rs: rs,
|
||||||
|
hn: hn,
|
||||||
|
rp: rp,
|
||||||
|
provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BaseRequestHandler) Shutdown() {
|
||||||
|
err := f.provider.Close()
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("handler shutdown error", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BaseRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
|
||||||
|
en := engine.NewEngine(cfg, rs)
|
||||||
|
en = en.WithPersister(pr)
|
||||||
|
return en
|
||||||
|
}
|
||||||
|
|
||||||
|
func(f *BaseRequestHandler) Process(rqs RequestSession) (RequestSession, error) {
|
||||||
|
var r bool
|
||||||
|
var err error
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs)
|
||||||
|
|
||||||
|
rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
|
||||||
|
return rqs, errors.ErrStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
//f.hn = f.hn.WithPersister(rqs.Storage.Persister)
|
||||||
|
f.hn.SetPersister(rqs.Storage.Persister)
|
||||||
|
defer func() {
|
||||||
|
f.hn.Exit()
|
||||||
|
}()
|
||||||
|
eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
|
||||||
|
en, ok := eni.(*engine.DefaultEngine)
|
||||||
|
if !ok {
|
||||||
|
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, errors.ErrEngineType
|
||||||
|
}
|
||||||
|
en = en.WithFirst(f.hn.Init)
|
||||||
|
if rqs.Config.EngineDebug {
|
||||||
|
en = en.WithDebug(nil)
|
||||||
|
}
|
||||||
|
rqs.Engine = en
|
||||||
|
|
||||||
|
r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
rqs.Continue = r
|
||||||
|
return rqs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func(f *BaseRequestHandler) Output(rqs RequestSession) (RequestSession, error) {
|
||||||
|
var err error
|
||||||
|
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
||||||
|
return rqs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func(f *BaseRequestHandler) Reset(rqs RequestSession) (RequestSession, error) {
|
||||||
|
defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
|
||||||
|
return rqs, rqs.Engine.Finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BaseRequestHandler) GetConfig() engine.Config {
|
||||||
|
return f.cfgTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
func(f *BaseRequestHandler) GetRequestParser() RequestParser {
|
||||||
|
return f.rp
|
||||||
|
}
|
37
request/http/parse.go
Normal file
37
request/http/parse.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultRequestParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
v := rqv.Header.Get("X-Vise-Session")
|
||||||
|
if v == "" {
|
||||||
|
return "", errors.ErrSessionMissing
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||||
|
rqv, ok := rq.(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
defer rqv.Body.Close()
|
||||||
|
v, err := ioutil.ReadAll(rqv.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
92
request/http/server.go
Normal file
92
request/http/server.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/request"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("visedriver.http.session")
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPRequestHandler implements the session handler for HTTP
|
||||||
|
type HTTPRequestHandler struct {
|
||||||
|
request.RequestHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *HTTPRequestHandler) WriteError(w http.ResponseWriter, code int, err error) {
|
||||||
|
s := err.Error()
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||||
|
w.WriteHeader(code)
|
||||||
|
_, err = w.Write([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPRequestHandler(h request.RequestHandler) *HTTPRequestHandler {
|
||||||
|
return &HTTPRequestHandler{
|
||||||
|
RequestHandler: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hh *HTTPRequestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
var code int
|
||||||
|
var err error
|
||||||
|
var perr error
|
||||||
|
|
||||||
|
rqs := request.RequestSession{
|
||||||
|
Ctx: req.Context(),
|
||||||
|
Writer: w,
|
||||||
|
}
|
||||||
|
|
||||||
|
rp := hh.GetRequestParser()
|
||||||
|
cfg := hh.GetConfig()
|
||||||
|
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
|
hh.WriteError(w, 400, err)
|
||||||
|
}
|
||||||
|
rqs.Config = cfg
|
||||||
|
rqs.Input, err = rp.GetInput(req)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
|
hh.WriteError(w, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rqs, err = hh.Process(rqs)
|
||||||
|
switch err {
|
||||||
|
case errors.ErrStorage:
|
||||||
|
code = 500
|
||||||
|
case errors.ErrEngineInit:
|
||||||
|
code = 500
|
||||||
|
case errors.ErrEngineExec:
|
||||||
|
code = 500
|
||||||
|
default:
|
||||||
|
code = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != 200 {
|
||||||
|
hh.WriteError(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
rqs, err = hh.Output(rqs)
|
||||||
|
rqs, perr = hh.Reset(rqs)
|
||||||
|
if err != nil {
|
||||||
|
hh.WriteError(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if perr != nil {
|
||||||
|
hh.WriteError(w, 500, perr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
233
request/http/server_test.go
Normal file
233
request/http/server_test.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
viseerrors "git.grassecon.net/grassrootseconomics/visedriver/errors"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/testutil/mocks/httpmocks"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 TestRequestHandler_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: viseerrors.ErrSessionMissing,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Process Error",
|
||||||
|
sessionID: "123",
|
||||||
|
input: []byte("test input"),
|
||||||
|
processErr: viseerrors.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 request.RequestSession) (request.RequestSession, error) {
|
||||||
|
return rs, tt.processErr
|
||||||
|
},
|
||||||
|
OutputFunc: func(rs request.RequestSession) (request.RequestSession, error) {
|
||||||
|
return rs, tt.outputErr
|
||||||
|
},
|
||||||
|
ResetFunc: func(rs request.RequestSession) (request.RequestSession, error) {
|
||||||
|
return rs, tt.resetErr
|
||||||
|
},
|
||||||
|
GetRequestParserFunc: func() request.RequestParser {
|
||||||
|
return mockRequestParser
|
||||||
|
},
|
||||||
|
GetConfigFunc: func() engine.Config {
|
||||||
|
return engine.Config{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionHandler := &HTTPRequestHandler{
|
||||||
|
RequestHandler: 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 TestRequestHandler_WriteError(t *testing.T) {
|
||||||
|
handler := &HTTPRequestHandler{}
|
||||||
|
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: viseerrors.ErrSessionMissing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Request Type",
|
||||||
|
request: invalidRequestType{},
|
||||||
|
expectedID: "",
|
||||||
|
expectedError: viseerrors.ErrInvalidRequest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := &DefaultRequestParser{}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
id, err := parser.GetSessionId(context.Background(),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: viseerrors.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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
42
request/request.go
Normal file
42
request/request.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
"git.defalsify.org/vise.git/persist"
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("visedriver.request")
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestSession struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Config engine.Config
|
||||||
|
Engine engine.Engine
|
||||||
|
Input []byte
|
||||||
|
Storage *storage.Storage
|
||||||
|
Writer io.Writer
|
||||||
|
Continue bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: seems like can remove this.
|
||||||
|
type RequestParser interface {
|
||||||
|
GetSessionId(ctx context.Context, rq any) (string, error)
|
||||||
|
GetInput(rq any) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestHandler interface {
|
||||||
|
GetConfig() engine.Config
|
||||||
|
GetRequestParser() RequestParser
|
||||||
|
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
||||||
|
Process(rs RequestSession) (RequestSession, error)
|
||||||
|
Output(rs RequestSession) (RequestSession, error)
|
||||||
|
Reset(rs RequestSession) (RequestSession, error)
|
||||||
|
Shutdown()
|
||||||
|
}
|
44
sample_tokens.json
Normal file
44
sample_tokens.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
# Variables to match files in the current directory
|
|
||||||
INPUTS = $(wildcard ./*.vis)
|
|
||||||
TXTS = $(wildcard ./*.txt.orig)
|
|
||||||
|
|
||||||
# Rule to build .bin files from .vis files
|
|
||||||
%.vis:
|
|
||||||
go run ../../go-vise/dev/asm $(basename $@).vis > $(basename $@).bin
|
|
||||||
@echo "Built $(basename $@).bin from $(basename $@).vis"
|
|
||||||
|
|
||||||
# Rule to copy .orig files to .txt
|
|
||||||
%.txt.orig:
|
|
||||||
cp -v $(basename $@).orig $(basename $@)
|
|
||||||
@echo "Copied $(basename $@).orig to $(basename $@)"
|
|
||||||
|
|
||||||
# 'all' target depends on all .vis and .txt.orig files
|
|
||||||
all: $(INPUTS) $(TXTS)
|
|
||||||
@echo "Running all: $(INPUTS) $(TXTS)"
|
|
@ -1 +0,0 @@
|
|||||||
Your account is being created...
|
|
@ -1,4 +0,0 @@
|
|||||||
RELOAD verify_pin
|
|
||||||
CATCH create_pin_mismatch 20 1
|
|
||||||
LOAD quit 0
|
|
||||||
HALT
|
|
@ -1 +0,0 @@
|
|||||||
Your account creation request failed. Please try again later.
|
|
@ -1,3 +0,0 @@
|
|||||||
MOUT quit 9
|
|
||||||
HALT
|
|
||||||
INCMP quit 9
|
|
@ -1 +0,0 @@
|
|||||||
Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye.
|
|
@ -1 +0,0 @@
|
|||||||
Akaunti yako inatengenezwa...
|
|
@ -1 +0,0 @@
|
|||||||
My Account
|
|
@ -1 +0,0 @@
|
|||||||
Akaunti yangu
|
|
@ -1 +0,0 @@
|
|||||||
Your account is still being created.
|
|
@ -1,3 +0,0 @@
|
|||||||
RELOAD check_account_status
|
|
||||||
CATCH main 11 1
|
|
||||||
HALT
|
|
@ -1 +0,0 @@
|
|||||||
Akaunti yako bado inatengenezwa
|
|
@ -1 +0,0 @@
|
|||||||
Address: {{.check_identifier}}
|
|
@ -1,6 +0,0 @@
|
|||||||
LOAD check_identifier 0
|
|
||||||
RELOAD check_identifier
|
|
||||||
MAP check_identifier
|
|
||||||
MOUT quit 9
|
|
||||||
HALT
|
|
||||||
INCMP quit 9
|
|
@ -1,2 +0,0 @@
|
|||||||
Maximum amount: {{.max_amount}}
|
|
||||||
Enter amount:
|
|
@ -1,12 +0,0 @@
|
|||||||
LOAD reset_transaction_amount 0
|
|
||||||
LOAD max_amount 10
|
|
||||||
MAP max_amount
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
LOAD validate_amount 64
|
|
||||||
RELOAD validate_amount
|
|
||||||
CATCH invalid_amount 17 1
|
|
||||||
INCMP _ 0
|
|
||||||
LOAD get_recipient 12
|
|
||||||
LOAD get_sender 64
|
|
||||||
INCMP transaction_pin *
|
|
@ -1,2 +0,0 @@
|
|||||||
Kiwango cha juu: {{.max_amount}}
|
|
||||||
Weka kiwango:
|
|
@ -1 +0,0 @@
|
|||||||
Back
|
|
@ -1 +0,0 @@
|
|||||||
Rudi
|
|
@ -1 +0,0 @@
|
|||||||
Balances:
|
|
@ -1,8 +0,0 @@
|
|||||||
LOAD reset_unlocked 0
|
|
||||||
MOUT my_balance 1
|
|
||||||
MOUT community_balance 2
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
INCMP my_balance 1
|
|
||||||
INCMP community_balance 2
|
|
@ -1 +0,0 @@
|
|||||||
Salio
|
|
@ -1 +0,0 @@
|
|||||||
Change language
|
|
@ -1 +0,0 @@
|
|||||||
Badili lugha
|
|
@ -1 +0,0 @@
|
|||||||
Change PIN
|
|
@ -1 +0,0 @@
|
|||||||
Badili PIN
|
|
@ -1 +0,0 @@
|
|||||||
Check balances
|
|
@ -1 +0,0 @@
|
|||||||
Angalia salio
|
|
@ -1 +0,0 @@
|
|||||||
Check statement
|
|
@ -1 +0,0 @@
|
|||||||
Taarifa ya matumizi
|
|
@ -1 +0,0 @@
|
|||||||
Salio la kikundi
|
|
@ -1,2 +0,0 @@
|
|||||||
Your community balance is: 0.00SRF
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
LOAD reset_incorrect 0
|
|
||||||
CATCH incorrect_pin 15 1
|
|
||||||
CATCH pin_entry 12 0
|
|
||||||
LOAD quit_with_balance 0
|
|
||||||
HALT
|
|
@ -1 +0,0 @@
|
|||||||
Community balance
|
|
@ -1 +0,0 @@
|
|||||||
Salio la kikundi
|
|
@ -1 +0,0 @@
|
|||||||
Enter your four number PIN again:
|
|
@ -1,4 +0,0 @@
|
|||||||
LOAD save_pin 0
|
|
||||||
HALT
|
|
||||||
LOAD verify_pin 8
|
|
||||||
INCMP account_creation *
|
|
@ -1 +0,0 @@
|
|||||||
Weka PIN yako tena:
|
|
@ -1 +0,0 @@
|
|||||||
Please enter a new four number PIN for your account:
|
|
@ -1,9 +0,0 @@
|
|||||||
LOAD create_account 0
|
|
||||||
CATCH account_creation_failed 22 1
|
|
||||||
MOUT exit 0
|
|
||||||
HALT
|
|
||||||
LOAD save_pin 0
|
|
||||||
RELOAD save_pin
|
|
||||||
CATCH . 15 1
|
|
||||||
INCMP quit 0
|
|
||||||
INCMP confirm_create_pin *
|
|
@ -1 +0,0 @@
|
|||||||
The PIN is not a match. Try again
|
|
@ -1,5 +0,0 @@
|
|||||||
MOUT retry 1
|
|
||||||
MOUT quit 9
|
|
||||||
HALT
|
|
||||||
INCMP confirm_create_pin 1
|
|
||||||
INCMP quit 9
|
|
@ -1 +0,0 @@
|
|||||||
PIN uliyoweka haifanani. Jaribu tena
|
|
@ -1 +0,0 @@
|
|||||||
Tafadhali weka PIN mpya yenye nambari nne kwa akaunti yako:
|
|
@ -1,5 +0,0 @@
|
|||||||
Wasifu wangu
|
|
||||||
Name: Not provided
|
|
||||||
Gender: Not provided
|
|
||||||
Age: Not provided
|
|
||||||
Location: Not provided
|
|
@ -1,3 +0,0 @@
|
|||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
@ -1 +0,0 @@
|
|||||||
Edit gender
|
|
@ -1 +0,0 @@
|
|||||||
Weka jinsia
|
|
@ -1 +0,0 @@
|
|||||||
Edit location
|
|
@ -1 +0,0 @@
|
|||||||
Weka eneo
|
|
@ -1 +0,0 @@
|
|||||||
Edit name
|
|
@ -1 +0,0 @@
|
|||||||
Weka jina
|
|
@ -1 +0,0 @@
|
|||||||
Edit offerings
|
|
@ -1 +0,0 @@
|
|||||||
Weka unachouza
|
|
@ -1 +0,0 @@
|
|||||||
My profile
|
|
@ -1,20 +0,0 @@
|
|||||||
LOAD reset_account_authorized 16
|
|
||||||
LOAD reset_allow_update 0
|
|
||||||
RELOAD reset_allow_update
|
|
||||||
MOUT edit_name 1
|
|
||||||
MOUT edit_gender 2
|
|
||||||
MOUT edit_yob 3
|
|
||||||
MOUT edit_location 4
|
|
||||||
MOUT edit_offerings 5
|
|
||||||
MOUT view 6
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
LOAD set_reset_single_edit 0
|
|
||||||
RELOAD set_reset_single_edit
|
|
||||||
INCMP enter_name 1
|
|
||||||
INCMP select_gender 2
|
|
||||||
INCMP enter_yob 3
|
|
||||||
INCMP enter_location 4
|
|
||||||
INCMP enter_offerings 5
|
|
||||||
INCMP view_profile 6
|
|
@ -1 +0,0 @@
|
|||||||
Wasifu wangu
|
|
@ -1 +0,0 @@
|
|||||||
Edit year of birth
|
|
@ -1 +0,0 @@
|
|||||||
Weka mwaka wa kuzaliwa
|
|
@ -1 +0,0 @@
|
|||||||
Enter family name:
|
|
@ -1,5 +0,0 @@
|
|||||||
LOAD save_firstname 0
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
INCMP select_gender *
|
|
@ -1 +0,0 @@
|
|||||||
Enter your location:
|
|
@ -1,12 +0,0 @@
|
|||||||
CATCH incorrect_date_format 21 1
|
|
||||||
LOAD save_yob 0
|
|
||||||
CATCH update_success 16 1
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
LOAD save_location 0
|
|
||||||
CATCH pin_entry 23 1
|
|
||||||
INCMP enter_offerings *
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
Weka eneo:
|
|
@ -1 +0,0 @@
|
|||||||
Enter your first names:
|
|
@ -1,4 +0,0 @@
|
|||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
INCMP _ 0
|
|
||||||
INCMP enter_familyname *
|
|
@ -1 +0,0 @@
|
|||||||
Weka majina yako ya kwanza:
|
|
@ -1 +0,0 @@
|
|||||||
Enter the services or goods you offer:
|
|
@ -1,8 +0,0 @@
|
|||||||
LOAD save_location 0
|
|
||||||
CATCH incorrect_pin 15 1
|
|
||||||
CATCH update_success 16 1
|
|
||||||
MOUT back 0
|
|
||||||
HALT
|
|
||||||
LOAD save_offerings 0
|
|
||||||
INCMP _ 0
|
|
||||||
INCMP pin_entry *
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user