Compare commits
1041 Commits
wip-flag-m
...
implement-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2d1f65bb3
|
||
|
|
bdde401439
|
||
|
|
a5c1ac2415
|
||
|
|
504e4baf56
|
||
|
|
d8a852575d
|
||
|
|
c8c0daac24 | ||
|
|
1bcffe2d23
|
||
| bf10e5357c | |||
|
|
d91c96f541 | ||
|
|
ec9032a42e | ||
|
|
6619afe62b | ||
|
|
1eb0b15552
|
||
|
|
ef803e0ee2
|
||
|
|
03d19283f6
|
||
|
|
15ce29a1a4
|
||
|
|
6749c632b0
|
||
|
|
8530c45074
|
||
|
|
d5e636fbd6
|
||
|
|
f7d31e4e81
|
||
|
|
90ecec1798
|
||
|
|
874edb3da6
|
||
|
|
60ff1b0ab3
|
||
|
|
9b3dad579b
|
||
|
|
348fff8936
|
||
|
|
c5bb1c80a5
|
||
|
|
b8a377befb
|
||
|
|
c9b92191f3
|
||
|
|
ddd8d7cac0
|
||
|
|
37973a6c9b
|
||
|
|
975720919c
|
||
|
|
c0534ede1b
|
||
|
|
24e729d275
|
||
|
|
ae6e2a99c5
|
||
|
|
7ca3974371
|
||
|
|
adbfab3964
|
||
|
|
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
|
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
|
||||
**/*/.state/
|
||||
cmd/.state/
|
||||
id_*
|
||||
*.gdbm
|
||||
*.log
|
||||
|
||||
172
cmd/main.go
172
cmd/main.go
@@ -1,172 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
file, err := os.Open(pfp)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to open CSV file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
// Iterate through the CSV records and register the flags
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Error reading CSV file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Ensure the record starts with "flag" and has at least 3 columns
|
||||
if len(record) < 3 || record[0] != "flag" {
|
||||
continue
|
||||
}
|
||||
|
||||
flagName := record[1]
|
||||
flagValue, err := strconv.Atoi(record[2])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to convert flag value %s to integer: %v\n", record[2], err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Register the flag
|
||||
state.FlagDebugger.Register(uint32(flagValue), flagName)
|
||||
}
|
||||
|
||||
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, err := ussd.NewHandlers(fp, &st,sessionId)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "handler setup failed with error: %v\n", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
164
config/config.go
164
config/config.go
@@ -1,10 +1,164 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
||||
const (
|
||||
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create"
|
||||
TrackStatusURL = "https://custodial.sarafu.africa/api/track/"
|
||||
BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/env"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("visedriver-config")
|
||||
defaultLanguage = "eng"
|
||||
languages []string
|
||||
DefaultLanguage string
|
||||
dbConn string
|
||||
dbConnMissing bool
|
||||
dbConnMode storage.DbMode
|
||||
stateDbConn string
|
||||
stateDbConnMode storage.DbMode
|
||||
resourceDbConn string
|
||||
resourceDbConnMode storage.DbMode
|
||||
userDbConn string
|
||||
userDbConnMode storage.DbMode
|
||||
Languages []string
|
||||
configManager *Config
|
||||
)
|
||||
|
||||
type Override struct {
|
||||
DbConn string
|
||||
DbConnMode storage.DbMode
|
||||
StateConn string
|
||||
StateConnMode storage.DbMode
|
||||
ResourceConn string
|
||||
ResourceConnMode storage.DbMode
|
||||
UserConn string
|
||||
UserConnMode storage.DbMode
|
||||
}
|
||||
|
||||
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", "?")
|
||||
stateDbConn = env.GetEnv("DB_CONN_STATE", dbConn)
|
||||
resourceDbConn = env.GetEnv("DB_CONN_RESOURCE", dbConn)
|
||||
userDbConn = env.GetEnv("DB_CONN_USER", dbConn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyConn(override *Override) {
|
||||
if override.DbConn != "?" {
|
||||
dbConn = override.DbConn
|
||||
stateDbConn = override.StateConn
|
||||
resourceDbConn = override.ResourceConn
|
||||
userDbConn = override.UserConn
|
||||
}
|
||||
dbConnMode = override.DbConnMode
|
||||
if override.StateConn != "?" {
|
||||
stateDbConn = override.StateConn
|
||||
}
|
||||
if override.ResourceConn != "?" {
|
||||
resourceDbConn = override.ResourceConn
|
||||
}
|
||||
if override.UserConn != "?" {
|
||||
userDbConn = override.UserConn
|
||||
}
|
||||
|
||||
if dbConn == "?" {
|
||||
dbConn = ""
|
||||
}
|
||||
|
||||
if stateDbConn == "?" {
|
||||
stateDbConn = dbConn
|
||||
stateDbConnMode = dbConnMode
|
||||
}
|
||||
if resourceDbConn == "?" {
|
||||
resourceDbConn = dbConn
|
||||
resourceDbConnMode = dbConnMode
|
||||
}
|
||||
if userDbConn == "?" {
|
||||
userDbConn = dbConn
|
||||
userDbConnMode = dbConnMode
|
||||
}
|
||||
|
||||
logg.Debugf("conns", "conn", dbConn, "user", userDbConn)
|
||||
if override.DbConnMode != storage.DBMODE_ANY {
|
||||
dbConnMode = override.DbConnMode
|
||||
}
|
||||
if override.StateConnMode != storage.DBMODE_ANY {
|
||||
stateDbConnMode = override.StateConnMode
|
||||
}
|
||||
if override.ResourceConnMode != storage.DBMODE_ANY {
|
||||
resourceDbConnMode = override.ResourceConnMode
|
||||
}
|
||||
if override.UserConnMode != storage.DBMODE_ANY {
|
||||
userDbConnMode = override.UserConnMode
|
||||
}
|
||||
}
|
||||
|
||||
func GetConns() (storage.Conns, error) {
|
||||
o := storage.NewConns()
|
||||
c, err := storage.ToConnDataMode(stateDbConn, stateDbConnMode)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
o.Set(c, storage.STORETYPE_STATE)
|
||||
c, err = storage.ToConnDataMode(resourceDbConn, resourceDbConnMode)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
o.Set(c, storage.STORETYPE_RESOURCE)
|
||||
c, err = storage.ToConnDataMode(userDbConn, userDbConnMode)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
o.Set(c, storage.STORETYPE_USER)
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// LoadConfig initializes the configuration values after environment variables are loaded.
|
||||
func LoadConfig() error {
|
||||
configManager = NewConfig(logg)
|
||||
|
||||
// Add configuration keys with validation
|
||||
configManager.AddKey("HOST", "127.0.0.1", false, nil)
|
||||
configManager.AddKey("PORT", "7123", false, func(v string) error {
|
||||
_, err := strconv.Atoi(v)
|
||||
return err
|
||||
})
|
||||
configManager.AddKey("DB_CONN", "", true, nil)
|
||||
// ... add other keys ? or is enough :/ ...
|
||||
|
||||
err := setConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setLanguage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
DefaultLanguage = defaultLanguage
|
||||
Languages = languages
|
||||
|
||||
// Report configuration
|
||||
configManager.Report("INFO")
|
||||
return nil
|
||||
}
|
||||
|
||||
63
config/config_test.go
Normal file
63
config/config_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// +build configreport
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
// go test -tags configreport ./config/... ---> run with tag
|
||||
func TestConfig(t *testing.T) {
|
||||
logger := logging.NewVanilla().WithDomain("test")
|
||||
cfg := NewConfig(logger)
|
||||
|
||||
t.Run("Default Values", func(t *testing.T) {
|
||||
cfg.AddKey("TEST_KEY", "default", false, nil)
|
||||
value, err := cfg.GetValue("TEST_KEY")
|
||||
t.Logf("Got value: %q, error: %v", value, err)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if value != "default" {
|
||||
t.Errorf("expected 'default', got '%s'", value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Environment Override", func(t *testing.T) {
|
||||
os.Setenv("TEST_ENV_KEY", "override")
|
||||
defer os.Unsetenv("TEST_ENV_KEY")
|
||||
|
||||
cfg.AddKey("TEST_ENV_KEY", "default", false, nil)
|
||||
value, err := cfg.GetValue("TEST_ENV_KEY")
|
||||
t.Logf("Got value: %q, error: %v", value, err)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if value != "override" {
|
||||
t.Errorf("expected 'override', got '%s'", value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Validation", func(t *testing.T) {
|
||||
validator := func(v string) error {
|
||||
if v != "valid" {
|
||||
return fmt.Errorf("invalid value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg.AddKey("VALIDATED_KEY", "valid", false, validator)
|
||||
os.Setenv("VALIDATED_KEY", "invalid")
|
||||
defer os.Unsetenv("VALIDATED_KEY")
|
||||
|
||||
value, err := cfg.GetValue("VALIDATED_KEY")
|
||||
t.Logf("Got value: %q, error: %v", value, err)
|
||||
if err == nil {
|
||||
t.Error("expected validation error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
95
config/reporter.go
Normal file
95
config/reporter.go
Normal file
@@ -0,0 +1,95 @@
|
||||
//go:build configreport
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/env"
|
||||
)
|
||||
|
||||
// ConfigValue represents a configuration key-value pair
|
||||
type ConfigValue struct {
|
||||
Key string
|
||||
Default string
|
||||
Validator func(string) error
|
||||
Sensitive bool
|
||||
}
|
||||
|
||||
// Config handles configuration management and reporting
|
||||
type Config struct {
|
||||
values map[string]ConfigValue
|
||||
logger logging.Vanilla
|
||||
}
|
||||
|
||||
func NewConfig(logger logging.Vanilla) *Config {
|
||||
return &Config{
|
||||
values: make(map[string]ConfigValue),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// AddKey registers a new configuration key with optional validation
|
||||
func (c *Config) AddKey(key string, defaultValue string, sensitive bool, validator func(string) error) {
|
||||
c.values[key] = ConfigValue{
|
||||
Key: key,
|
||||
Default: defaultValue,
|
||||
Validator: validator,
|
||||
Sensitive: sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
// GetValue returns the value for a given key, applying environment override if present
|
||||
func (c *Config) GetValue(key string) (string, error) {
|
||||
// Find config value by key
|
||||
var cv ConfigValue
|
||||
for _, v := range c.values {
|
||||
if v.Key == key {
|
||||
cv = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cv.Key == "" {
|
||||
return "", fmt.Errorf("configuration key not found: %s", key)
|
||||
}
|
||||
|
||||
// Get value from environment or default
|
||||
value := env.GetEnv(key, cv.Default)
|
||||
|
||||
// Validate if validator exists
|
||||
if cv.Validator != nil && cv.Validator(value) != nil {
|
||||
return "", fmt.Errorf("invalid value for key %s", key)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Report outputs all configuration values at the specified log level
|
||||
func (c *Config) Report(level string) {
|
||||
for _, cv := range c.values {
|
||||
value, err := c.GetValue(cv.Key)
|
||||
if err != nil {
|
||||
c.logger.Errorf("Error getting value for %s: %v", cv.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if cv.Sensitive {
|
||||
value = "****"
|
||||
}
|
||||
|
||||
switch level {
|
||||
case "DEBUG":
|
||||
c.logger.Debugf("config set", cv.Key, value)
|
||||
case "INFO":
|
||||
c.logger.Infof("config set", cv.Key, value)
|
||||
case "WARN":
|
||||
c.logger.Warnf("config set", cv.Key, value)
|
||||
case "ERROR":
|
||||
c.logger.Errorf("config set", cv.Key, value)
|
||||
default:
|
||||
c.logger.Infof("config set", cv.Key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
config/reporter_noop.go
Normal file
21
config/reporter_noop.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build !configreport
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func NewConfig(logger logging.Vanilla) *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
func (c *Config) AddKey(key string, defaultValue string, sensitive bool, validator func(string) error) {}
|
||||
|
||||
func (c *Config) GetValue(key string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *Config) Report(level string) {}
|
||||
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.
|
||||
14
entry/handlers.go
Normal file
14
entry/handlers.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
)
|
||||
|
||||
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
Submodule go-vise deleted from 1f47a674d9
28
go.mod
28
go.mod
@@ -1,5 +1,27 @@
|
||||
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.3.2-0.20250407143413-e55cf9bcb7d2
|
||||
github.com/jackc/pgx/v5 v5.7.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // 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/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 // indirect
|
||||
)
|
||||
|
||||
49
go.sum
49
go.sum
@@ -1,2 +1,51 @@
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250204132233-2bffe532f21e h1:gtB9OdX6x5gQRM3W824dEurXuuf/YPInqgtv2KAp5Zo=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250204132233-2bffe532f21e/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
git.defalsify.org/vise.git v0.3.2-0.20250407143413-e55cf9bcb7d2 h1:kbiDZtvphEKsTAnebrB6QxRbB7zdoTHSmzzumXrJ4hw=
|
||||
git.defalsify.org/vise.git v0.3.2-0.20250407143413-e55cf9bcb7d2/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
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/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/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/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/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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/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/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/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,909 +0,0 @@
|
||||
package ussd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/asm"
|
||||
"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/utils"
|
||||
"github.com/graygnuorg/go-gdbm"
|
||||
"gopkg.in/leonelquinteros/gotext.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
scriptDir = path.Join("services", "registration")
|
||||
translationDir = path.Join(scriptDir, "locale")
|
||||
//dbFile = path.Join(scriptDir, "userdata.gdbm")
|
||||
)
|
||||
|
||||
const (
|
||||
TrackingIdKey = "TRACKINGID"
|
||||
PublicKeyKey = "PUBLICKEY"
|
||||
CustodialIdKey = "CUSTODIALID"
|
||||
AccountPin = "ACCOUNTPIN"
|
||||
AccountStatus = "ACCOUNTSTATUS"
|
||||
FirstName = "FIRSTNAME"
|
||||
FamilyName = "FAMILYNAME"
|
||||
YearOfBirth = "YOB"
|
||||
Location = "LOCATION"
|
||||
Gender = "GENDER"
|
||||
Offerings = "OFFERINGS"
|
||||
Recipient = "RECIPIENT"
|
||||
Amount = "AMOUNT"
|
||||
AccountCreated = "ACCOUNTCREATED"
|
||||
)
|
||||
|
||||
func toBytes(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
type FSData struct {
|
||||
Path string
|
||||
St *state.State
|
||||
}
|
||||
|
||||
type FlagParserInterface interface {
|
||||
GetFlag(key string) (uint32, error)
|
||||
}
|
||||
|
||||
type Handlers struct {
|
||||
fs *FSData
|
||||
db *gdbm.Database
|
||||
parser FlagParserInterface
|
||||
accountFileHandler utils.AccountFileHandlerInterface
|
||||
accountService server.AccountServiceInterface
|
||||
}
|
||||
|
||||
func NewHandlers(dir string, st *state.State, sessionId string) (*Handlers, error) {
|
||||
filename := path.Join(scriptDir, sessionId+"_userdata.gdbm")
|
||||
db, err := gdbm.Open(filename, gdbm.ModeWrcreat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
parser := asm.NewFlagParser()
|
||||
_, err = parser.Load(pfp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Handlers{
|
||||
db: db,
|
||||
fs: &FSData{
|
||||
Path: dir,
|
||||
St: st,
|
||||
},
|
||||
parser: parser,
|
||||
accountFileHandler: utils.NewAccountFileHandler(dir + "_data"),
|
||||
accountService: &server.AccountService{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (h *Handlers) PreloadFlags(flagKeys []string) (map[string]uint32, error) {
|
||||
flags := make(map[string]uint32)
|
||||
for _, key := range flagKeys {
|
||||
flag, err := h.parser.GetFlag(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flags[key] = flag
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
// SetLanguage sets the language across the menu
|
||||
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
res := resource.Result{}
|
||||
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_language_set"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
inputStr := string(input)
|
||||
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, flags["flag_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{}
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_account_created", "flag_account_creation_failed"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
_, err = h.db.Fetch([]byte(AccountCreated))
|
||||
if err != nil {
|
||||
if errors.Is(err, gdbm.ErrItemNotFound) {
|
||||
accountResp, err := h.accountService.CreateAccount()
|
||||
if err != nil {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_account_creation_failed"])
|
||||
return res, err
|
||||
}
|
||||
data := map[string]string{
|
||||
TrackingIdKey: accountResp.Result.TrackingId,
|
||||
PublicKeyKey: accountResp.Result.PublicKey,
|
||||
CustodialIdKey: accountResp.Result.CustodialId.String(),
|
||||
}
|
||||
|
||||
for key, value := range data {
|
||||
err := h.db.Store(toBytes(key), toBytes(value), true)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
key := []byte(AccountCreated)
|
||||
value := []byte("1")
|
||||
h.db.Store(key, value, true)
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_account_created"])
|
||||
return res, err
|
||||
} else {
|
||||
return res, err
|
||||
}
|
||||
} else {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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{}
|
||||
flagKeys := []string{"flag_incorrect_pin"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
accountPIN := string(input)
|
||||
// Validate that the PIN is a 4-digit number
|
||||
if !isValidPIN(accountPIN) {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_incorrect_pin"])
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_incorrect_pin"])
|
||||
|
||||
key := []byte(AccountPin)
|
||||
value := []byte(accountPIN)
|
||||
|
||||
h.db.Store(key, value, true)
|
||||
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)
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_allow_update", "flag_single_edit"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
switch menuOption {
|
||||
case "2":
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_allow_update"])
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_single_edit"])
|
||||
case "3":
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_allow_update"])
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_single_edit"])
|
||||
case "4":
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_allow_update"])
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_single_edit"])
|
||||
default:
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_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{}
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_valid_pin", "flag_pin_mismatch", "flag_pin_set"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
AccountPin, err := h.db.Fetch([]byte(AccountPin))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if bytes.Equal(input, AccountPin) {
|
||||
res.FlagSet = []uint32{flags["flag_valid_pin"]}
|
||||
res.FlagReset = []uint32{flags["flag_pin_mismatch"]}
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_pin_set"])
|
||||
} else {
|
||||
res.FlagSet = []uint32{flags["flag_pin_mismatch"]}
|
||||
}
|
||||
|
||||
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{}
|
||||
if len(input) > 0 {
|
||||
name := string(input)
|
||||
key := []byte(FirstName)
|
||||
value := []byte(name)
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
|
||||
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{}
|
||||
if len(input) > 0 {
|
||||
secondname := string(input)
|
||||
key := []byte(FamilyName)
|
||||
value := []byte(secondname)
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
|
||||
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{}
|
||||
yob := string(input)
|
||||
if len(yob) == 4 {
|
||||
yob := string(input)
|
||||
key := []byte(YearOfBirth)
|
||||
value := []byte(yob)
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
|
||||
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{}
|
||||
if len(input) > 0 {
|
||||
location := string(input)
|
||||
key := []byte(Location)
|
||||
value := []byte(location)
|
||||
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
|
||||
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{}
|
||||
if len(input) > 0 {
|
||||
gender := string(input)
|
||||
switch gender {
|
||||
case "1":
|
||||
gender = "Male"
|
||||
case "2":
|
||||
gender = "Female"
|
||||
case "3":
|
||||
gender = "Unspecified"
|
||||
}
|
||||
key := []byte(Gender)
|
||||
value := []byte(gender)
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
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{}
|
||||
if len(input) > 0 {
|
||||
offerings := string(input)
|
||||
key := []byte(Offerings)
|
||||
value := []byte(offerings)
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
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{}
|
||||
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_allow_update"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_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{}
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_account_authorized"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_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{}
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Content = string(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{}
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_incorrect_pin", "flag_account_authorized", "flag_allow_update"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
storedpin, err := h.db.Fetch([]byte(AccountPin))
|
||||
if err == nil {
|
||||
if len(input) == 4 {
|
||||
if bytes.Equal(input, storedpin) {
|
||||
if h.fs.St.MatchFlag(flags["flag_account_authorized"], false) {
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_incorrect_pin"])
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_allow_update"], flags["flag_account_authorized"])
|
||||
} else {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_allow_update"])
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_account_authorized"])
|
||||
}
|
||||
} else {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_incorrect_pin"])
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_account_authorized"])
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
} else if errors.Is(err, gdbm.ErrItemNotFound) {
|
||||
return res, err
|
||||
} else {
|
||||
return res, err
|
||||
}
|
||||
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{}
|
||||
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_incorrect_pin"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_incorrect_pin"])
|
||||
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{}
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_account_success", "flag_account_pending"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
trackingId, err := h.db.Fetch([]byte(TrackingIdKey))
|
||||
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
status, err := h.accountService.CheckAccountStatus(string(trackingId))
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error checking account status:", err)
|
||||
return res, err
|
||||
|
||||
}
|
||||
|
||||
err = h.db.Store(toBytes(AccountStatus), toBytes(status), true)
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
err = h.db.Store(toBytes(TrackingIdKey), toBytes(status), true)
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if status == "SUCCESS" {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_account_success"])
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_account_pending"])
|
||||
} else {
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_account_success"])
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_account_pending"])
|
||||
}
|
||||
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{}
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_account_authorized"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
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, flags["flag_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{}
|
||||
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_incorrect_date_format"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
date := string(input)
|
||||
_, err = strconv.Atoi(date)
|
||||
if err != nil {
|
||||
// If conversion fails, input is not numeric
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_incorrect_date_format"])
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if len(date) == 4 {
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_incorrect_date_format"])
|
||||
} else {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_incorrect_date_format"])
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ResetIncorrectYob resets the incorrect date format flag after a new attempt
|
||||
func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
res := resource.Result{}
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_incorrect_date_format"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_incorrect_date_format"])
|
||||
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{}
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
balance, err := h.accountService.CheckBalance(string(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)
|
||||
|
||||
flagKeys := []string{"flag_invalid_recipient"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if recipient != "0" {
|
||||
// mimic invalid number check
|
||||
if recipient == "000" {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_invalid_recipient"])
|
||||
res.Content = recipient
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// accountData["Recipient"] = recipient
|
||||
key := []byte(Recipient)
|
||||
value := []byte(recipient)
|
||||
|
||||
h.db.Store(key, value, true)
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
// Preload the required flags
|
||||
flagKeys := []string{"flag_invalid_recipient", "flag_invalid_recipient_with_invite"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = h.db.Delete([]byte(Amount))
|
||||
if err != nil && !errors.Is(err, gdbm.ErrItemNotFound) {
|
||||
return res, err
|
||||
}
|
||||
err = h.db.Delete([]byte(Recipient))
|
||||
if err != nil && !errors.Is(err, gdbm.ErrItemNotFound) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_invalid_recipient"], flags["flag_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{}
|
||||
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_invalid_amount"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = h.db.Delete([]byte(Amount))
|
||||
if err != nil && !errors.Is(err, gdbm.ErrItemNotFound) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_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{}
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
balance, err := h.accountService.CheckBalance(string(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{}
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_invalid_amount"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
amountStr := string(input)
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
balanceStr, err := h.accountService.CheckBalance(string(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, flags["flag_invalid_amount"])
|
||||
res.Content = amountStr
|
||||
return res, nil
|
||||
}
|
||||
|
||||
inputAmount, err := strconv.ParseFloat(matches[1], 64)
|
||||
if err != nil {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_invalid_amount"])
|
||||
res.Content = amountStr
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if inputAmount > balanceValue {
|
||||
res.FlagSet = append(res.FlagSet, flags["flag_invalid_amount"])
|
||||
res.Content = amountStr
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
|
||||
key := []byte(Amount)
|
||||
value := []byte(res.Content)
|
||||
h.db.Store(key, value, true)
|
||||
|
||||
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{}
|
||||
recipient, err := h.db.Fetch([]byte(Recipient))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.Content = string(recipient)
|
||||
|
||||
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()
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.Content = string(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{}
|
||||
amount, err := h.db.Fetch([]byte(Amount))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Content = string(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{}
|
||||
|
||||
// Preload the required flag
|
||||
flagKeys := []string{"flag_account_authorized"}
|
||||
flags, err := h.PreloadFlags(flagKeys)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
code := codeFromCtx(ctx)
|
||||
l := gotext.NewLocale(translationDir, code)
|
||||
l.AddDomain("default")
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
balance, err := h.accountService.CheckBalance(string(publicKey))
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
res.Content = l.Get("Your account balance is %s", balance)
|
||||
res.FlagReset = append(res.FlagReset, flags["flag_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")
|
||||
// TODO
|
||||
// Use the amount, recipient and sender to call the API and initialize the transaction
|
||||
|
||||
publicKey, err := h.db.Fetch([]byte(PublicKeyKey))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
amount, err := h.db.Fetch([]byte(Amount))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
recipient, err := h.db.Fetch([]byte(Recipient))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(publicKey))
|
||||
|
||||
account_authorized_flag, err := h.parser.GetFlag("flag_account_authorized")
|
||||
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, account_authorized_flag)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetProfileInfo retrieves and formats the profile information of a user from a Gdbm backed storage.
|
||||
func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
res := resource.Result{}
|
||||
|
||||
// Define default values
|
||||
defaultValue := "Not provided"
|
||||
name := defaultValue
|
||||
familyName := defaultValue
|
||||
yob := defaultValue
|
||||
gender := defaultValue
|
||||
location := defaultValue
|
||||
offerings := defaultValue
|
||||
|
||||
// Fetch data using a map for better organization
|
||||
dataKeys := map[string]*string{
|
||||
FirstName: &name,
|
||||
FamilyName: &familyName,
|
||||
YearOfBirth: &yob,
|
||||
Location: &location,
|
||||
Gender: &gender,
|
||||
Offerings: &offerings,
|
||||
}
|
||||
|
||||
// Iterate over keys and fetch values
|
||||
//iter := h.db.Iterator()
|
||||
next := h.db.Iterator()
|
||||
//defer iter.Close() // Ensure the iterator is closed
|
||||
for key, err := next(); err == nil; key, err = next() {
|
||||
if valuePointer, ok := dataKeys[string(key)]; ok {
|
||||
value, fetchErr := h.db.Fetch(key)
|
||||
if fetchErr == nil {
|
||||
*valuePointer = string(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the full name
|
||||
if familyName != defaultValue {
|
||||
if name == defaultValue {
|
||||
name = familyName
|
||||
} else {
|
||||
name = name + " " + familyName
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate age from year of birth
|
||||
var age string
|
||||
if yob != defaultValue {
|
||||
yobInt, err := strconv.Atoi(yob)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("invalid year of birth: %v", err)
|
||||
}
|
||||
age = strconv.Itoa(utils.CalculateAgeWithYOB(yobInt))
|
||||
} else {
|
||||
age = defaultValue
|
||||
}
|
||||
|
||||
// Format the result
|
||||
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
|
||||
}
|
||||
@@ -1,993 +0,0 @@
|
||||
package ussd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
|
||||
type MockFlagParser struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockFlagParser) GetFlag(key string) (uint32, error) {
|
||||
args := m.Called(key)
|
||||
return args.Get(0).(uint32), args.Error(1)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
mockParser := new(MockFlagParser)
|
||||
|
||||
flag_account_created := uint32(1)
|
||||
flag_account_creation_failed := uint32(2)
|
||||
|
||||
mockParser.On("GetFlag", "flag_account_created").Return(flag_account_created, nil)
|
||||
mockParser.On("GetFlag", "flag_account_creation_failed").Return(flag_account_creation_failed, nil)
|
||||
|
||||
// Initialize Handlers with mock account service
|
||||
h := &Handlers{
|
||||
fs: &FSData{Path: accountFilePath},
|
||||
accountFileHandler: accountFileHandler,
|
||||
accountService: mockAccountService,
|
||||
parser: mockParser,
|
||||
}
|
||||
|
||||
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{flag_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)
|
||||
mockParser := new(MockFlagParser)
|
||||
|
||||
h := &Handlers{
|
||||
accountFileHandler: accountFileHandler,
|
||||
parser: mockParser,
|
||||
}
|
||||
|
||||
flag_incorrect_pin := uint32(1)
|
||||
mockParser.On("GetFlag", "flag_incorrect_pin").Return(flag_incorrect_pin, nil)
|
||||
|
||||
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{flag_incorrect_pin},
|
||||
expectedData: initialAccountData, // No changes expected
|
||||
expectedErrors: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN - less than 4 digits",
|
||||
input: []byte("123"),
|
||||
expectedFlags: []uint32{flag_incorrect_pin},
|
||||
expectedData: initialAccountData, // No changes expected
|
||||
expectedErrors: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid PIN - more than 4 digits",
|
||||
input: []byte("12345"),
|
||||
expectedFlags: []uint32{flag_incorrect_pin},
|
||||
expectedData: initialAccountData, // No changes expected
|
||||
expectedErrors: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProfileInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
accountData map[string]string
|
||||
readError error
|
||||
expectedResult resource.Result
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Complete Profile",
|
||||
accountData: map[string]string{
|
||||
"FirstName": "John",
|
||||
"FamilyName": "Doe",
|
||||
"Gender": "Male",
|
||||
"YOB": "1980",
|
||||
"Location": "Mombasa",
|
||||
"Offerings": "Product A",
|
||||
},
|
||||
readError: nil,
|
||||
expectedResult: resource.Result{
|
||||
Content: fmt.Sprintf(
|
||||
"Name: %s %s\nGender: %s\nAge: %d\nLocation: %s\nYou provide: %s\n",
|
||||
"John", "Doe", "Male", 44, "Mombasa", "Product A",
|
||||
),
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Profile with Not Provided Fields",
|
||||
accountData: map[string]string{
|
||||
"FirstName": "Not provided",
|
||||
"FamilyName": "Doe",
|
||||
"Gender": "Female",
|
||||
"YOB": "1995",
|
||||
"Location": "Not provided",
|
||||
"Offerings": "Service B",
|
||||
},
|
||||
readError: nil,
|
||||
expectedResult: resource.Result{
|
||||
Content: fmt.Sprintf(
|
||||
"Name: %s\nGender: %s\nAge: %d\nLocation: %s\nYou provide: %s\n",
|
||||
"Not provided", "Female", 29, "Not provided", "Service B",
|
||||
),
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Profile with YOB as Not provided",
|
||||
accountData: map[string]string{
|
||||
"FirstName": "Not provided",
|
||||
"FamilyName": "Doe",
|
||||
"Gender": "Female",
|
||||
"YOB": "Not provided",
|
||||
"Location": "Not provided",
|
||||
"Offerings": "Service B",
|
||||
},
|
||||
readError: nil,
|
||||
expectedResult: resource.Result{
|
||||
Content: fmt.Sprintf(
|
||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||
"Not provided", "Female", "Not provided", "Not provided", "Service B",
|
||||
),
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a new instance of MockAccountFileHandler
|
||||
mockFileHandler := new(mocks.MockAccountFileHandler)
|
||||
|
||||
// Set up the mock expectations
|
||||
mockFileHandler.On("ReadAccountData").Return(tt.accountData, tt.readError)
|
||||
|
||||
// Create the Handlers instance with the mock file handler
|
||||
h := &Handlers{
|
||||
accountFileHandler: mockFileHandler,
|
||||
}
|
||||
|
||||
// Call the method
|
||||
result, err := h.GetProfileInfo(context.Background(), "get_profile_info", nil)
|
||||
|
||||
// Assert the results
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
assert.Equal(t, tt.expectedError, err)
|
||||
|
||||
// Assert all expectations were met
|
||||
mockFileHandler.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
|
||||
|
||||
113
request/base.go
Normal file
113
request/base.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/entry"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/errors"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/storage"
|
||||
)
|
||||
|
||||
type EngineFunc func(engine.Config, resource.Resource, *persist.Persister) engine.Engine
|
||||
|
||||
type BaseRequestHandler struct {
|
||||
cfgTemplate engine.Config
|
||||
rp RequestParser
|
||||
rs resource.Resource
|
||||
hn entry.EntryHandler
|
||||
provider storage.StorageProvider
|
||||
engineFunc EngineFunc
|
||||
}
|
||||
|
||||
func NewBaseRequestHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn entry.EntryHandler) *BaseRequestHandler {
|
||||
h := &BaseRequestHandler{
|
||||
cfgTemplate: cfg,
|
||||
rs: rs,
|
||||
hn: hn,
|
||||
rp: rp,
|
||||
provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
|
||||
}
|
||||
h.engineFunc = h.getDefaultEngine
|
||||
return h
|
||||
}
|
||||
|
||||
func (f *BaseRequestHandler) WithEngineFunc(fn EngineFunc) *BaseRequestHandler {
|
||||
f.engineFunc = fn
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *BaseRequestHandler) Shutdown(ctx context.Context) {
|
||||
err := f.provider.Close(ctx)
|
||||
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 {
|
||||
return f.engineFunc(cfg, rs, pr)
|
||||
}
|
||||
|
||||
func (f *BaseRequestHandler) getDefaultEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
|
||||
en := engine.NewEngine(cfg, rs)
|
||||
en = en.WithPersister(pr)
|
||||
en = en.WithFirst(f.hn.Init)
|
||||
if f.cfgTemplate.EngineDebug {
|
||||
en = en.WithDebug(nil)
|
||||
}
|
||||
return en
|
||||
}
|
||||
|
||||
func (f *BaseRequestHandler) Process(rqs RequestSession) (RequestSession, error) {
|
||||
var r bool
|
||||
var err error
|
||||
|
||||
logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs)
|
||||
|
||||
rqs.Storage, err = f.provider.Get(rqs.Ctx, rqs.Config.SessionId)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
|
||||
return rqs, errors.ErrStorage
|
||||
}
|
||||
|
||||
f.hn.SetPersister(rqs.Storage.Persister)
|
||||
defer func() {
|
||||
f.hn.Exit()
|
||||
}()
|
||||
|
||||
rqs.Engine = f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
|
||||
r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
|
||||
if err != nil {
|
||||
perr := f.provider.Put(rqs.Ctx, 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(ctx context.Context, rqs RequestSession) (RequestSession, error) {
|
||||
defer f.provider.Put(ctx, rqs.Config.SessionId, rqs.Storage)
|
||||
return rqs, rqs.Engine.Finish(ctx)
|
||||
}
|
||||
|
||||
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/errors"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/request"
|
||||
)
|
||||
|
||||
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.Ctx, 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/request"
|
||||
"git.grassecon.net/grassrootseconomics/visedriver/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func 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(ctx context.Context, 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/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"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(context.Context, any) (string, error)
|
||||
GetInput(any) ([]byte, error)
|
||||
}
|
||||
|
||||
type RequestHandler interface {
|
||||
GetConfig() engine.Config
|
||||
GetRequestParser() RequestParser
|
||||
GetEngine(engine.Config, resource.Resource, *persist.Persister) engine.Engine
|
||||
Process(RequestSession) (RequestSession, error)
|
||||
Output(RequestSession) (RequestSession, error)
|
||||
Reset(context.Context, RequestSession) (RequestSession, error)
|
||||
Shutdown(ctx context.Context)
|
||||
}
|
||||
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 -f pp.csv $(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 flag_pin_mismatch 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 flag_account_success 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 flag_invalid_amount 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_account_authorized 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 flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 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 flag_account_creation_failed 1
|
||||
MOUT exit 0
|
||||
HALT
|
||||
LOAD save_pin 0
|
||||
RELOAD save_pin
|
||||
CATCH . flag_incorrect_pin 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,9 +0,0 @@
|
||||
CATCH incorrect_date_format flag_incorrect_date_format 1
|
||||
LOAD save_yob 0
|
||||
CATCH update_success flag_allow_update 1
|
||||
MOUT back 0
|
||||
HALT
|
||||
INCMP _ 0
|
||||
LOAD save_location 0
|
||||
CATCH pin_entry flag_single_edit 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:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user