forked from grassrootseconomics/visedriver
		
	Compare commits
	
		
			946 Commits
		
	
	
		
			wip-pin-re
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 770fef68b9 | |||
| dd2751839d | |||
| 5a9c822075 | |||
| 
						 | 
					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 | |||
| a3f2b23128 | |||
| 09f970db9b | |||
| 04edd90c21 | |||
| a3f410875a | |||
| 3f3e98e637 | |||
| 660f8b7aa2 | 
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
@ -1,257 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/asm"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	gdbmdb "git.defalsify.org/vise.git/db/gdbm"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type atRequestParser struct {}
 | 
			
		||||
 | 
			
		||||
func(arp *atRequestParser) GetSessionId(rq any) (string, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	if err := rqv.ParseForm(); err != nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to parse form data: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	phoneNumber := rqv.FormValue("phoneNumber")
 | 
			
		||||
	if phoneNumber == "" {
 | 
			
		||||
		return "", fmt.Errorf("no phone number found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return phoneNumber, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(arp *atRequestParser) GetInput(rq any) ([]byte, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	if err := rqv.ParseForm(); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to parse form data: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	text := rqv.FormValue("text")
 | 
			
		||||
 | 
			
		||||
	parts := strings.Split(text, "*")
 | 
			
		||||
	if len(parts) == 0 {
 | 
			
		||||
		return nil, fmt.Errorf("no input found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return []byte(parts[len(parts)-1]), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
 | 
			
		||||
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
 | 
			
		||||
	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
 | 
			
		||||
	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
 | 
			
		||||
	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
 | 
			
		||||
	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
 | 
			
		||||
	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
 | 
			
		||||
	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
 | 
			
		||||
	rs.AddLocalFunc("quit", ussdHandlers.Quit)
 | 
			
		||||
	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
 | 
			
		||||
	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
 | 
			
		||||
	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
 | 
			
		||||
	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
 | 
			
		||||
	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
 | 
			
		||||
	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
 | 
			
		||||
	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
 | 
			
		||||
	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
 | 
			
		||||
	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
 | 
			
		||||
	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
 | 
			
		||||
	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
 | 
			
		||||
	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
 | 
			
		||||
	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
 | 
			
		||||
	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
 | 
			
		||||
	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
 | 
			
		||||
	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
 | 
			
		||||
	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
 | 
			
		||||
	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
 | 
			
		||||
	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
 | 
			
		||||
	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
 | 
			
		||||
	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
 | 
			
		||||
 | 
			
		||||
	return ussdHandlers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ensureDbDir(dbDir string) error {
 | 
			
		||||
	err := os.MkdirAll(dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "state.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	return store, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "userdata.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	
 | 
			
		||||
	return store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	store := fsdb.NewFsDb()
 | 
			
		||||
	err := store.Connect(ctx, resourceDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rfs := resource.NewDbResource(store)
 | 
			
		||||
	return rfs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var stateDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output")
 | 
			
		||||
	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", "127.0.0.1", "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", 7123, "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir,  "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
	flagParser, err := getFlags(pfp, true)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(16),
 | 
			
		||||
	}
 | 
			
		||||
	if stateDebug {
 | 
			
		||||
		cfg.StateDebug = true
 | 
			
		||||
	}
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := getResource(resourceDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = ensureDbDir(dbDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdataStore := getUserdataDb(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer userdataStore.Close()
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := getHandler(flagParser, dbResource, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := getStateStore(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer stateStore.Close()
 | 
			
		||||
 | 
			
		||||
	rp := &atRequestParser{}
 | 
			
		||||
	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
 | 
			
		||||
	sh := httpserver.NewATSessionHandler(bsh)
 | 
			
		||||
	s := &http.Server{
 | 
			
		||||
		Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
 | 
			
		||||
		Handler: sh,
 | 
			
		||||
	}
 | 
			
		||||
	s.RegisterOnShutdown(sh.Shutdown)
 | 
			
		||||
 | 
			
		||||
	cint := make(chan os.Signal)
 | 
			
		||||
	cterm := make(chan os.Signal)
 | 
			
		||||
	signal.Notify(cint, os.Interrupt, syscall.SIGINT)
 | 
			
		||||
	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
	go func() {
 | 
			
		||||
		select {
 | 
			
		||||
		case _ = <-cint:
 | 
			
		||||
		case _ = <-cterm:
 | 
			
		||||
		}
 | 
			
		||||
		s.Shutdown(ctx)
 | 
			
		||||
	}()
 | 
			
		||||
	err = s.ListenAndServe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Infof("Server closed with error", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,251 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/asm"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	gdbmdb "git.defalsify.org/vise.git/db/gdbm"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type asyncRequestParser struct {
 | 
			
		||||
	sessionId string
 | 
			
		||||
	input []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(p *asyncRequestParser) GetSessionId(r any) (string, error) {
 | 
			
		||||
	return p.sessionId, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(p *asyncRequestParser) GetInput(r any) ([]byte, error) {
 | 
			
		||||
	return p.input, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
 | 
			
		||||
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
 | 
			
		||||
	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
 | 
			
		||||
	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
 | 
			
		||||
	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
 | 
			
		||||
	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
 | 
			
		||||
	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
 | 
			
		||||
	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
 | 
			
		||||
	rs.AddLocalFunc("quit", ussdHandlers.Quit)
 | 
			
		||||
	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
 | 
			
		||||
	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
 | 
			
		||||
	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
 | 
			
		||||
	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
 | 
			
		||||
	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
 | 
			
		||||
	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
 | 
			
		||||
	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
 | 
			
		||||
	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
 | 
			
		||||
	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
 | 
			
		||||
	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
 | 
			
		||||
	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
 | 
			
		||||
	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
 | 
			
		||||
	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
 | 
			
		||||
	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
 | 
			
		||||
	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
 | 
			
		||||
	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
 | 
			
		||||
	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
 | 
			
		||||
	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
 | 
			
		||||
	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
 | 
			
		||||
	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
 | 
			
		||||
	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
 | 
			
		||||
 | 
			
		||||
	return ussdHandlers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ensureDbDir(dbDir string) error {
 | 
			
		||||
	err := os.MkdirAll(dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "state.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	return store, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "userdata.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	
 | 
			
		||||
	return store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	store := fsdb.NewFsDb()
 | 
			
		||||
	err := store.Connect(ctx, resourceDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rfs := resource.NewDbResource(store)
 | 
			
		||||
	return rfs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var stateDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output")
 | 
			
		||||
	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", "127.0.0.1", "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", 7123, "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir,  "outputsize", size, "sessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
	flagParser, err := getFlags(pfp, true)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(16),
 | 
			
		||||
	}
 | 
			
		||||
	if stateDebug {
 | 
			
		||||
		cfg.StateDebug = true
 | 
			
		||||
	}
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := getResource(resourceDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = ensureDbDir(dbDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdataStore := getUserdataDb(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer userdataStore.Close()
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := getHandler(flagParser, dbResource, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := getStateStore(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer stateStore.Close()
 | 
			
		||||
 | 
			
		||||
	rp := &asyncRequestParser{
 | 
			
		||||
		sessionId: sessionId,
 | 
			
		||||
	}
 | 
			
		||||
	sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
 | 
			
		||||
	cfg.SessionId = sessionId
 | 
			
		||||
	rqs := handlers.RequestSession{
 | 
			
		||||
		Ctx: ctx,
 | 
			
		||||
		Writer: os.Stdout,
 | 
			
		||||
		Config: cfg,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cint := make(chan os.Signal)
 | 
			
		||||
	cterm := make(chan os.Signal)
 | 
			
		||||
	signal.Notify(cint, os.Interrupt, syscall.SIGINT)
 | 
			
		||||
	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
	go func() {
 | 
			
		||||
		select {
 | 
			
		||||
		case _ = <-cint:
 | 
			
		||||
		case _ = <-cterm:
 | 
			
		||||
		}
 | 
			
		||||
		sh.Shutdown()
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	for true {
 | 
			
		||||
		rqs, err = sh.Process(rqs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Errorf("error in process: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		rqs, err = sh.Output(rqs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Errorf("error in output: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		rqs, err = sh.Reset(rqs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Errorf("error in reset: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		_, err = fmt.Scanln(&rqs.Input)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		fmt.Errorf("error in input: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										217
									
								
								cmd/http/main.go
									
									
									
									
									
								
							
							
						
						
									
										217
									
								
								cmd/http/main.go
									
									
									
									
									
								
							@ -1,217 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/asm"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	gdbmdb "git.defalsify.org/vise.git/db/gdbm"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getFlags(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) {
 | 
			
		||||
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
 | 
			
		||||
	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
 | 
			
		||||
	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
 | 
			
		||||
	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
 | 
			
		||||
	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
 | 
			
		||||
	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
 | 
			
		||||
	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
 | 
			
		||||
	rs.AddLocalFunc("quit", ussdHandlers.Quit)
 | 
			
		||||
	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
 | 
			
		||||
	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
 | 
			
		||||
	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
 | 
			
		||||
	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
 | 
			
		||||
	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
 | 
			
		||||
	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
 | 
			
		||||
	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
 | 
			
		||||
	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
 | 
			
		||||
	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
 | 
			
		||||
	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
 | 
			
		||||
	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
 | 
			
		||||
	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
 | 
			
		||||
	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
 | 
			
		||||
	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
 | 
			
		||||
	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
 | 
			
		||||
	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
 | 
			
		||||
	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
 | 
			
		||||
	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
 | 
			
		||||
	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
 | 
			
		||||
	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
 | 
			
		||||
	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
 | 
			
		||||
 | 
			
		||||
	return ussdHandlers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ensureDbDir(dbDir string) error {
 | 
			
		||||
	err := os.MkdirAll(dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getStateStore(dbDir string, ctx context.Context) (db.Db, error) {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "state.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	return store, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "userdata.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	
 | 
			
		||||
	return store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	store := fsdb.NewFsDb()
 | 
			
		||||
	err := store.Connect(ctx, resourceDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rfs := resource.NewDbResource(store)
 | 
			
		||||
	return rfs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	var stateDebug bool
 | 
			
		||||
	var host string
 | 
			
		||||
	var port uint
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output")
 | 
			
		||||
	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", "127.0.0.1", "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", 7123, "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir,  "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
	flagParser, err := getFlags(pfp, true)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(16),
 | 
			
		||||
	}
 | 
			
		||||
	if stateDebug {
 | 
			
		||||
		cfg.StateDebug = true
 | 
			
		||||
	}
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := getResource(resourceDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = ensureDbDir(dbDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdataStore := getUserdataDb(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer userdataStore.Close()
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := getHandler(flagParser, dbResource, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := getStateStore(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer stateStore.Close()
 | 
			
		||||
 | 
			
		||||
	rp := &httpserver.DefaultRequestParser{}
 | 
			
		||||
	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
 | 
			
		||||
	sh := httpserver.ToSessionHandler(bsh)
 | 
			
		||||
	s := &http.Server{
 | 
			
		||||
		Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
 | 
			
		||||
		Handler: sh,
 | 
			
		||||
	}
 | 
			
		||||
	s.RegisterOnShutdown(sh.Shutdown)
 | 
			
		||||
 | 
			
		||||
	cint := make(chan os.Signal)
 | 
			
		||||
	cterm := make(chan os.Signal)
 | 
			
		||||
	signal.Notify(cint, os.Interrupt, syscall.SIGINT)
 | 
			
		||||
	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
	go func() {
 | 
			
		||||
		select {
 | 
			
		||||
		case _ = <-cint:
 | 
			
		||||
		case _ = <-cterm:
 | 
			
		||||
		}
 | 
			
		||||
		s.Shutdown(ctx)
 | 
			
		||||
	}()
 | 
			
		||||
	err = s.ListenAndServe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Infof("Server closed with error", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										198
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										198
									
								
								cmd/main.go
									
									
									
									
									
								
							@ -1,198 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/asm"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	gdbmdb "git.defalsify.org/vise.git/db/gdbm"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.Persister, userdataStore db.Db) (*ussd.Handlers, error) {
 | 
			
		||||
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	ussdHandlers = ussdHandlers.WithPersister(pe)
 | 
			
		||||
	rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage)
 | 
			
		||||
	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
 | 
			
		||||
	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
 | 
			
		||||
	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
 | 
			
		||||
	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
 | 
			
		||||
	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
 | 
			
		||||
	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
 | 
			
		||||
	rs.AddLocalFunc("quit", ussdHandlers.Quit)
 | 
			
		||||
	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
 | 
			
		||||
	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
 | 
			
		||||
	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
 | 
			
		||||
	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
 | 
			
		||||
	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
 | 
			
		||||
	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
 | 
			
		||||
	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
 | 
			
		||||
	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
 | 
			
		||||
	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
 | 
			
		||||
	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
 | 
			
		||||
	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
 | 
			
		||||
	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
 | 
			
		||||
	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
 | 
			
		||||
	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
 | 
			
		||||
	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance)
 | 
			
		||||
	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
 | 
			
		||||
	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
 | 
			
		||||
	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
 | 
			
		||||
	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
 | 
			
		||||
	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
 | 
			
		||||
	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit)
 | 
			
		||||
	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
 | 
			
		||||
 | 
			
		||||
	return ussdHandlers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ensureDbDir(dbDir string) error {
 | 
			
		||||
	err := os.MkdirAll(dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) {
 | 
			
		||||
	err := ensureDbDir(dbDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "state.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
	pr := persist.NewPersister(store)
 | 
			
		||||
	return pr, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserdataDb(dbDir string, ctx context.Context) db.Db {
 | 
			
		||||
	store := gdbmdb.NewGdbmDb()
 | 
			
		||||
	storeFile := path.Join(dbDir, "userdata.gdbm")
 | 
			
		||||
	store.Connect(ctx, storeFile)
 | 
			
		||||
 | 
			
		||||
	return store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	store := fsdb.NewFsDb()
 | 
			
		||||
	err := store.Connect(ctx, resourceDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rfs := resource.NewDbResource(store)
 | 
			
		||||
	return rfs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine {
 | 
			
		||||
	en := engine.NewEngine(cfg, rs)
 | 
			
		||||
	en = en.WithPersister(pr)
 | 
			
		||||
	return en
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var debug bool
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.BoolVar(&debug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
	flagParser, err := getParser(pfp, true)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(16),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := getResource(scriptDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := getPersister(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store := getUserdataDb(dbDir, ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := getHandler(flagParser, dbResource, pe, store)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	en := getEngine(cfg, rs, pe)
 | 
			
		||||
	en = en.WithFirst(hl.Init)
 | 
			
		||||
	if debug {
 | 
			
		||||
		en = en.WithDebug(nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = en.Init(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.grassecon.net/grassrootseconomics/visedriver/env"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	slogging "github.com/grassrootseconomics/go-vise/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg               = slogging.Get().With("component", "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()
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										64
									
								
								config/config_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								config/config_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
//go:build configreport
 | 
			
		||||
// +build configreport
 | 
			
		||||
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	slogging "github.com/grassrootseconomics/go-vise/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// go test -tags configreport ./config/...   ---> run with tag
 | 
			
		||||
func TestConfig(t *testing.T) {
 | 
			
		||||
	logger := slogging.Get().With("component", "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")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								config/reporter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								config/reporter.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
//go:build configreport
 | 
			
		||||
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/env"
 | 
			
		||||
	slogging "github.com/grassrootseconomics/go-vise/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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 *slogging.Slog
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewConfig(logging interface{}) *Config {
 | 
			
		||||
	return &Config{
 | 
			
		||||
		values: make(map[string]ConfigValue),
 | 
			
		||||
		logger: slogging.Get().With("component", "visedriver-config-reporter"),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 = "****"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.logger.Debugf("config set", cv.Key, value)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								config/reporter_noop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								config/reporter_noop.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
//go:build !configreport
 | 
			
		||||
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
type Config struct{}
 | 
			
		||||
 | 
			
		||||
func NewConfig() *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 `github.com/grassrootseconomics/go-vise/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"
 | 
			
		||||
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/persist"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/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")
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										35
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								go.mod
									
									
									
									
									
								
							@ -1,26 +1,27 @@
 | 
			
		||||
module git.grassecon.net/urdt/ussd
 | 
			
		||||
module git.grassecon.net/grassrootseconomics/visedriver
 | 
			
		||||
 | 
			
		||||
go 1.22.6
 | 
			
		||||
go 1.24
 | 
			
		||||
 | 
			
		||||
toolchain go1.24.6
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f
 | 
			
		||||
	github.com/alecthomas/assert/v2 v2.2.2
 | 
			
		||||
	github.com/peteole/testdata-loader v0.3.0
 | 
			
		||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.1
 | 
			
		||||
	github.com/grassrootseconomics/go-vise v0.5.0
 | 
			
		||||
	github.com/jackc/pgx/v5 v5.7.5
 | 
			
		||||
	github.com/joho/godotenv v1.5.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/alecthomas/participle/v2 v2.0.0 // indirect
 | 
			
		||||
	github.com/alecthomas/repr v0.2.0 // indirect
 | 
			
		||||
	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
			
		||||
	github.com/fxamacker/cbor/v2 v2.4.0 // indirect
 | 
			
		||||
	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
 | 
			
		||||
	github.com/hexops/gotextdiff v1.0.3 // indirect
 | 
			
		||||
	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
 | 
			
		||||
	github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
			
		||||
	github.com/stretchr/objx v0.5.2 // indirect
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 | 
			
		||||
	github.com/fxamacker/cbor/v2 v2.9.0 // 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/leonelquinteros/gotext v1.7.2 // 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
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.40.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.16.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.27.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								go.sum
									
									
									
									
									
								
							@ -1,36 +1,47 @@
 | 
			
		||||
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f h1:CuJvG3NyMoRtHUim4aZdrfjjJBg2AId7z0yp7Q97bRM=
 | 
			
		||||
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 | 
			
		||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
 | 
			
		||||
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
 | 
			
		||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
 | 
			
		||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 | 
			
		||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
 | 
			
		||||
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 | 
			
		||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 | 
			
		||||
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/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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
 | 
			
		||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 | 
			
		||||
github.com/grassrootseconomics/go-vise v0.5.0 h1:FRg2de55Eb5SisrgTBeFWfWX+sXwp5q9r7YWtKWDwsk=
 | 
			
		||||
github.com/grassrootseconomics/go-vise v0.5.0/go.mod h1:b2/q4jfTu2i1wyUwYUu7FYq4m2f1AZv8MpiDM3ZcoGo=
 | 
			
		||||
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.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
 | 
			
		||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
 | 
			
		||||
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/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
 | 
			
		||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
 | 
			
		||||
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
 | 
			
		||||
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
 | 
			
		||||
github.com/pashagolub/pgxmock/v4 v4.7.0 h1:de2ORuFYyjwOQR7NBm57+321RnZxpYiuUjsmqRiqgh8=
 | 
			
		||||
github.com/pashagolub/pgxmock/v4 v4.7.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM=
 | 
			
		||||
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 | 
			
		||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 | 
			
		||||
github.com/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=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 | 
			
		||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
 | 
			
		||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
 | 
			
		||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
 | 
			
		||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 | 
			
		||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
 | 
			
		||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
 | 
			
		||||
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,117 +0,0 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BaseSessionHandler struct {
 | 
			
		||||
	cfgTemplate engine.Config
 | 
			
		||||
	rp RequestParser
 | 
			
		||||
	rs resource.Resource
 | 
			
		||||
	hn *ussd.Handlers
 | 
			
		||||
	provider storage.StorageProvider
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler {
 | 
			
		||||
	return &BaseSessionHandler{
 | 
			
		||||
		cfgTemplate: cfg,
 | 
			
		||||
		rs: rs,
 | 
			
		||||
		hn: hn,
 | 
			
		||||
		rp: rp,
 | 
			
		||||
		provider: storage.NewSimpleStorageProvider(stateDb, userdataDb),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f* BaseSessionHandler) Shutdown() {
 | 
			
		||||
	err := f.provider.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("handler shutdown error", "err", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine {
 | 
			
		||||
	en := engine.NewEngine(cfg, rs)
 | 
			
		||||
	en = en.WithPersister(pr)
 | 
			
		||||
	return en
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
	var r bool
 | 
			
		||||
	var err error
 | 
			
		||||
	var ok bool
 | 
			
		||||
	
 | 
			
		||||
	logg.InfoCtxf(rqs.Ctx, "new request",  rqs)
 | 
			
		||||
 | 
			
		||||
	rqs.Storage, err = f.provider.Get(rqs.Config.SessionId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err)
 | 
			
		||||
		return rqs, ErrStorage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f.hn = f.hn.WithPersister(rqs.Storage.Persister)
 | 
			
		||||
	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
 | 
			
		||||
	en, ok := eni.(*engine.DefaultEngine)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
		rqs.Storage = nil
 | 
			
		||||
		if perr != nil {
 | 
			
		||||
			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
 | 
			
		||||
		}
 | 
			
		||||
		return rqs, ErrEngineType
 | 
			
		||||
	}
 | 
			
		||||
	en = en.WithFirst(f.hn.Init)
 | 
			
		||||
	if rqs.Config.EngineDebug {
 | 
			
		||||
		en = en.WithDebug(nil)
 | 
			
		||||
	}
 | 
			
		||||
	rqs.Engine = en
 | 
			
		||||
 | 
			
		||||
	r, err = rqs.Engine.Init(rqs.Ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
		rqs.Storage = nil
 | 
			
		||||
		if perr != nil {
 | 
			
		||||
			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
 | 
			
		||||
		}
 | 
			
		||||
		return rqs, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r && len(rqs.Input) > 0 {
 | 
			
		||||
		r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
		rqs.Storage = nil
 | 
			
		||||
		if perr != nil {
 | 
			
		||||
			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
 | 
			
		||||
		}
 | 
			
		||||
		return rqs, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rqs.Continue = r 
 | 
			
		||||
	return rqs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer)
 | 
			
		||||
	return rqs, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) {
 | 
			
		||||
	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
	return rqs, rqs.Engine.Finish()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) GetConfig() engine.Config {
 | 
			
		||||
	return f.cfgTemplate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) GetRequestParser() RequestParser {
 | 
			
		||||
	return f.rp
 | 
			
		||||
}
 | 
			
		||||
@ -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,56 +0,0 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("handlers")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RequestSession struct {
 | 
			
		||||
	Ctx context.Context
 | 
			
		||||
	Config engine.Config
 | 
			
		||||
	Engine engine.Engine
 | 
			
		||||
	Input []byte
 | 
			
		||||
	Storage *storage.Storage
 | 
			
		||||
	Writer io.Writer
 | 
			
		||||
	Continue bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type engineMaker func(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine
 | 
			
		||||
 | 
			
		||||
// TODO: seems like can remove this.
 | 
			
		||||
type RequestParser interface {
 | 
			
		||||
	GetSessionId(rq any) (string, error)
 | 
			
		||||
	GetInput(rq any) ([]byte, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RequestHandler interface {
 | 
			
		||||
	GetConfig() engine.Config
 | 
			
		||||
	GetRequestParser() RequestParser
 | 
			
		||||
	GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine 
 | 
			
		||||
	Process(rs RequestSession) (RequestSession, error)
 | 
			
		||||
	Output(rs RequestSession) (RequestSession, error)
 | 
			
		||||
	Reset(rs RequestSession) (RequestSession, error)
 | 
			
		||||
	Shutdown()
 | 
			
		||||
}
 | 
			
		||||
@ -1,970 +0,0 @@
 | 
			
		||||
package ussd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/asm"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/cache"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"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/server"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
	"gopkg.in/leonelquinteros/gotext.v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg           = logging.NewVanilla().WithDomain("ussdmenuhandler")
 | 
			
		||||
	scriptDir      = path.Join("services", "registration")
 | 
			
		||||
	translationDir = path.Join(scriptDir, "locale")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FSData struct {
 | 
			
		||||
	Path string
 | 
			
		||||
	St   *state.State
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FlagManager handles centralized flag management
 | 
			
		||||
type FlagManager struct {
 | 
			
		||||
	parser *asm.FlagParser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewFlagManager creates a new FlagManager instance
 | 
			
		||||
func NewFlagManager(csvPath string) (*FlagManager, error) {
 | 
			
		||||
	parser := asm.NewFlagParser()
 | 
			
		||||
	_, err := parser.Load(csvPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to load flag parser: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &FlagManager{
 | 
			
		||||
		parser: parser,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFlag retrieves a flag value by its label
 | 
			
		||||
func (fm *FlagManager) GetFlag(label string) (uint32, error) {
 | 
			
		||||
	return fm.parser.GetFlag(label)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Handlers struct {
 | 
			
		||||
	pe             *persist.Persister
 | 
			
		||||
	st             *state.State
 | 
			
		||||
	ca             cache.Memory
 | 
			
		||||
	userdataStore  utils.DataStore
 | 
			
		||||
	flagManager    *asm.FlagParser
 | 
			
		||||
	accountService server.AccountServiceInterface
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, error) {
 | 
			
		||||
	if userdataStore == nil {
 | 
			
		||||
		return nil, fmt.Errorf("cannot create handler with nil userdata store")
 | 
			
		||||
	}
 | 
			
		||||
	userDb := &utils.UserDataStore{
 | 
			
		||||
		Db: userdataStore,
 | 
			
		||||
	}
 | 
			
		||||
	h := &Handlers{
 | 
			
		||||
		userdataStore:  userDb,
 | 
			
		||||
		flagManager:    appFlags,
 | 
			
		||||
		accountService: &server.AccountService{},
 | 
			
		||||
	}
 | 
			
		||||
	return h, 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) WithPersister(pe *persist.Persister) *Handlers {
 | 
			
		||||
	if h.pe != nil {
 | 
			
		||||
		panic("persister already set")
 | 
			
		||||
	}
 | 
			
		||||
	h.pe = pe
 | 
			
		||||
	return h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var r resource.Result
 | 
			
		||||
 | 
			
		||||
	if h.pe == nil {
 | 
			
		||||
		logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca)
 | 
			
		||||
		return r, nil
 | 
			
		||||
	}
 | 
			
		||||
	h.st = h.pe.GetState()
 | 
			
		||||
	h.ca = h.pe.GetMemory()
 | 
			
		||||
	if h.st == nil || h.ca == nil {
 | 
			
		||||
		logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca)
 | 
			
		||||
		return r, fmt.Errorf("cannot get state and memory for handler")
 | 
			
		||||
	}
 | 
			
		||||
	h.pe = nil
 | 
			
		||||
 | 
			
		||||
	logg.DebugCtxf(ctx, "handler has been initialized", "state", h.st, "cache", h.ca)
 | 
			
		||||
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetLanguage sets the language across the menu
 | 
			
		||||
func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	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:
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	languageSetFlag, err := h.flagManager.GetFlag("flag_language_set")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
	res.FlagSet = append(res.FlagSet, languageSetFlag)
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
 | 
			
		||||
	accountResp, err := h.accountService.CreateAccount()
 | 
			
		||||
	data := map[utils.DataTyp]string{
 | 
			
		||||
		utils.DATA_TRACKING_ID:  accountResp.Result.TrackingId,
 | 
			
		||||
		utils.DATA_PUBLIC_KEY:   accountResp.Result.PublicKey,
 | 
			
		||||
		utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for key, value := range data {
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err := store.WriteEntry(ctx, sessionId, key, []byte(value))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
 | 
			
		||||
	res.FlagSet = append(res.FlagSet, flag_account_created)
 | 
			
		||||
	return err
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	_, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_CREATED)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if db.IsNotFound(err) {
 | 
			
		||||
			logg.Printf(logging.LVL_INFO, "Creating an account because it doesn't exist")
 | 
			
		||||
			err = h.createAccountNoExist(ctx, sessionId, &res)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return res, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
 | 
			
		||||
 | 
			
		||||
	accountPIN := string(input)
 | 
			
		||||
	// Validate that the PIN is a 4-digit number
 | 
			
		||||
	if !isValidPIN(accountPIN) {
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetResetSingleEdit sets and resets  flags to allow gradual editing of profile information.
 | 
			
		||||
func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	menuOption := string(input)
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
 | 
			
		||||
	flag_single_edit, _ := h.flagManager.GetFlag("flag_single_edit")
 | 
			
		||||
 | 
			
		||||
	switch menuOption {
 | 
			
		||||
	case "2":
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_allow_update)
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_single_edit)
 | 
			
		||||
	case "3":
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_allow_update)
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_single_edit)
 | 
			
		||||
	case "4":
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_allow_update)
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_single_edit)
 | 
			
		||||
	default:
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
 | 
			
		||||
	flag_pin_mismatch, _ := h.flagManager.GetFlag("flag_pin_mismatch")
 | 
			
		||||
	flag_pin_set, _ := h.flagManager.GetFlag("flag_pin_set")
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN)
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if bytes.Equal(input, AccountPin) {
 | 
			
		||||
		res.FlagSet = []uint32{flag_valid_pin}
 | 
			
		||||
		res.FlagReset = []uint32{flag_pin_mismatch}
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_pin_set)
 | 
			
		||||
	} else {
 | 
			
		||||
		res.FlagSet = []uint32{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
 | 
			
		||||
	if ctx.Value("Language") != nil {
 | 
			
		||||
		lang := ctx.Value("Language").(lang.Language)
 | 
			
		||||
		code = lang.Code
 | 
			
		||||
	}
 | 
			
		||||
	return code
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveFirstname updates the first name in the gdbm with the provided input.
 | 
			
		||||
func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(input) > 0 {
 | 
			
		||||
		firstName := string(input)
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveFamilyname updates the family name in the gdbm with the provided input.
 | 
			
		||||
func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(input) > 0 {
 | 
			
		||||
		familyName := string(input)
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(familyName))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, err
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, nil
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return res, fmt.Errorf("a family name cannot be less than one character")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveYOB updates the Year of Birth(YOB) in the gdbm with the provided input.
 | 
			
		||||
func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(input) == 4 {
 | 
			
		||||
		yob := string(input)
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_YOB, []byte(yob))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveLocation updates the location in the gdbm with the provided input.
 | 
			
		||||
func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(input) > 0 {
 | 
			
		||||
		location := string(input)
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_LOCATION, []byte(location))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveGender updates the gender in the gdbm with the provided input.
 | 
			
		||||
func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(input) > 0 {
 | 
			
		||||
		gender := string(input)
 | 
			
		||||
		switch gender {
 | 
			
		||||
		case "1":
 | 
			
		||||
			gender = "Male"
 | 
			
		||||
		case "2":
 | 
			
		||||
			gender = "Female"
 | 
			
		||||
		case "3":
 | 
			
		||||
			gender = "Unspecified"
 | 
			
		||||
		}
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveOfferings updates the offerings(goods and services provided by the user) in the gdbm with the provided input.
 | 
			
		||||
func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(input) > 0 {
 | 
			
		||||
		offerings := string(input)
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_OFFERINGS, []byte(offerings))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
 | 
			
		||||
	flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
 | 
			
		||||
	flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
 | 
			
		||||
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		if len(input) == 4 {
 | 
			
		||||
			if bytes.Equal(input, AccountPin) {
 | 
			
		||||
				if h.st.MatchFlag(flag_account_authorized, false) {
 | 
			
		||||
					res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
 | 
			
		||||
					res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
 | 
			
		||||
				} else {
 | 
			
		||||
					res.FlagSet = append(res.FlagSet, flag_allow_update)
 | 
			
		||||
					res.FlagReset = append(res.FlagReset, flag_account_authorized)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
 | 
			
		||||
				res.FlagReset = append(res.FlagReset, flag_account_authorized)
 | 
			
		||||
				return res, nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_account_success, _ := h.flagManager.GetFlag("flag_account_success")
 | 
			
		||||
	flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending")
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID)
 | 
			
		||||
	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 = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if status == "SUCCESS" {
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_account_success)
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_account_pending)
 | 
			
		||||
	} else {
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_account_success)
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
 | 
			
		||||
 | 
			
		||||
	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, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	flag_incorrect_date_format, _ := h.flagManager.GetFlag("flag_incorrect_date_format")
 | 
			
		||||
 | 
			
		||||
	date := string(input)
 | 
			
		||||
	_, err = strconv.Atoi(date)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// If conversion fails, input is not numeric
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_incorrect_date_format)
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(date) == 4 {
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_incorrect_date_format)
 | 
			
		||||
	} else {
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	flag_incorrect_date_format, _ := h.flagManager.GetFlag("flag_incorrect_date_format")
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	recipient := string(input)
 | 
			
		||||
 | 
			
		||||
	flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
 | 
			
		||||
 | 
			
		||||
	if recipient != "0" {
 | 
			
		||||
		// mimic invalid number check
 | 
			
		||||
		if recipient == "000" {
 | 
			
		||||
			res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
 | 
			
		||||
			res.Content = recipient
 | 
			
		||||
 | 
			
		||||
			return res, nil
 | 
			
		||||
		}
 | 
			
		||||
		store := h.userdataStore
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_RECIPIENT, []byte(recipient))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
 | 
			
		||||
	flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_RECIPIENT, []byte(""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, flag_invalid_recipient, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.FlagReset = append(res.FlagReset, 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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
 | 
			
		||||
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
 | 
			
		||||
	amountStr := string(input)
 | 
			
		||||
 | 
			
		||||
	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, flag_invalid_amount)
 | 
			
		||||
		res.Content = amountStr
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	inputAmount, err := strconv.ParseFloat(matches[1], 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_invalid_amount)
 | 
			
		||||
		res.Content = amountStr
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if inputAmount > balanceValue {
 | 
			
		||||
		res.FlagSet = append(res.FlagSet, flag_invalid_amount)
 | 
			
		||||
		res.Content = amountStr
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRecipient returns the transaction recipient from the gdbm.
 | 
			
		||||
func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT)
 | 
			
		||||
 | 
			
		||||
	res.Content = string(recipient)
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSender retrieves the public key from the Gdbm Db
 | 
			
		||||
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
 | 
			
		||||
	res.Content = string(publicKey)
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAmount retrieves the amount from teh Gdbm Db
 | 
			
		||||
func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
 | 
			
		||||
 | 
			
		||||
	code := codeFromCtx(ctx)
 | 
			
		||||
	l := gotext.NewLocale(translationDir, code)
 | 
			
		||||
	l.AddDomain("default")
 | 
			
		||||
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
	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, flag_account_authorized)
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InitiateTransaction returns a confirmation and resets the transaction data
 | 
			
		||||
// on the gdbm store.
 | 
			
		||||
func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	var err error
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
 | 
			
		||||
	amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
 | 
			
		||||
 | 
			
		||||
	recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT)
 | 
			
		||||
 | 
			
		||||
	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.flagManager.GetFlag("flag_account_authorized")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Default value when an entry is not found
 | 
			
		||||
	defaultValue := "Not Provided"
 | 
			
		||||
 | 
			
		||||
	// Helper function to handle nil byte slices and convert them to string
 | 
			
		||||
	getEntryOrDefault := func(entry []byte, err error) string {
 | 
			
		||||
		if err != nil || entry == nil {
 | 
			
		||||
			return defaultValue
 | 
			
		||||
		}
 | 
			
		||||
		return string(entry)
 | 
			
		||||
	}
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	// Retrieve user data as strings with fallback to defaultValue
 | 
			
		||||
	firstName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_FIRST_NAME))
 | 
			
		||||
	familyName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_FAMILY_NAME))
 | 
			
		||||
	yob := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_YOB))
 | 
			
		||||
	gender := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_GENDER))
 | 
			
		||||
	location := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_LOCATION))
 | 
			
		||||
	offerings := getEntryOrDefault(store.ReadEntry(ctx, sessionId, utils.DATA_OFFERINGS))
 | 
			
		||||
 | 
			
		||||
	// Construct the full name
 | 
			
		||||
	name := defaultValue
 | 
			
		||||
	if familyName != defaultValue {
 | 
			
		||||
		if firstName == defaultValue {
 | 
			
		||||
			name = familyName
 | 
			
		||||
		} else {
 | 
			
		||||
			name = firstName + " " + familyName
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Calculate age from year of birth
 | 
			
		||||
	age := defaultValue
 | 
			
		||||
	if yob != defaultValue {
 | 
			
		||||
		if yobInt, err := strconv.Atoi(yob); err == nil {
 | 
			
		||||
			age = strconv.Itoa(utils.CalculateAgeWithYOB(yobInt))
 | 
			
		||||
		} else {
 | 
			
		||||
			return res, fmt.Errorf("invalid year of birth: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Format the result
 | 
			
		||||
	res.Content = fmt.Sprintf(
 | 
			
		||||
		"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
 | 
			
		||||
		name, gender, age, location, offerings,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,59 +0,0 @@
 | 
			
		||||
package mocks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
	"github.com/stretchr/testify/mock"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MockDb struct {
 | 
			
		||||
	mock.Mock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) SetPrefix(prefix uint8) {
 | 
			
		||||
	m.Called(prefix)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) Prefix() uint8 {
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Get(0).(uint8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) Safe() bool {
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Get(0).(bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) SetLanguage(language *lang.Language) {
 | 
			
		||||
	m.Called(language)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) SetLock(uint8, bool) error {
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Error(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) Connect(ctx context.Context, connectionStr string) error {
 | 
			
		||||
	args := m.Called(ctx, connectionStr)
 | 
			
		||||
	return args.Error(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) SetSession(sessionId string) {
 | 
			
		||||
	m.Called(sessionId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) Put(ctx context.Context, key, value []byte) error {
 | 
			
		||||
	args := m.Called(ctx, key, value)
 | 
			
		||||
	return args.Error(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) {
 | 
			
		||||
	args := m.Called(ctx, key)
 | 
			
		||||
	return nil, args.Error(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockDb) Close() error {
 | 
			
		||||
	args := m.Called(nil)
 | 
			
		||||
	return args.Error(0)
 | 
			
		||||
}
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
package mocks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/models"
 | 
			
		||||
	"github.com/stretchr/testify/mock"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MockAccountService implements AccountServiceInterface for testing
 | 
			
		||||
type MockAccountService struct {
 | 
			
		||||
	mock.Mock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Get(0).(*models.AccountResponse), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CheckBalance(publicKey string) (string, error) {
 | 
			
		||||
	args := m.Called(publicKey)
 | 
			
		||||
	return args.String(0), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) {
 | 
			
		||||
	args := m.Called(trackingId)
 | 
			
		||||
	return args.String(0), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
package mocks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
	"github.com/stretchr/testify/mock"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MockUserDataStore struct {
 | 
			
		||||
	db.Db
 | 
			
		||||
	mock.Mock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) {
 | 
			
		||||
	args := m.Called(ctx, sessionId, typ)
 | 
			
		||||
	return args.Get(0).([]byte), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error {
 | 
			
		||||
	args := m.Called(ctx, sessionId, typ, value)
 | 
			
		||||
	return args.Error(0)
 | 
			
		||||
}
 | 
			
		||||
@ -1,93 +0,0 @@
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ATSessionHandler struct {
 | 
			
		||||
	*SessionHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
 | 
			
		||||
	return &ATSessionHandler{
 | 
			
		||||
		SessionHandler: ToSessionHandler(h),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	var code int
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	rqs := handlers.RequestSession{
 | 
			
		||||
		Ctx:    req.Context(),
 | 
			
		||||
		Writer: w,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rp := ash.GetRequestParser()
 | 
			
		||||
	cfg := ash.GetConfig()
 | 
			
		||||
	cfg.SessionId, err = rp.GetSessionId(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
 | 
			
		||||
		ash.writeError(w, 400, err)
 | 
			
		||||
	}
 | 
			
		||||
	rqs.Config = cfg
 | 
			
		||||
	rqs.Input, err = rp.GetInput(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
 | 
			
		||||
		ash.writeError(w, 400, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rqs, err = ash.Process(rqs)
 | 
			
		||||
	switch err {
 | 
			
		||||
	case handlers.ErrStorage:
 | 
			
		||||
		code = 500
 | 
			
		||||
	case handlers.ErrEngineInit:
 | 
			
		||||
		code = 500
 | 
			
		||||
	case handlers.ErrEngineExec:
 | 
			
		||||
		code = 500
 | 
			
		||||
	default:
 | 
			
		||||
		code = 200
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if code != 200 {
 | 
			
		||||
		ash.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(200)
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain")
 | 
			
		||||
	rqs, err = ash.Output(rqs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ash.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rqs, err = ash.Reset(rqs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ash.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	var prefix string
 | 
			
		||||
 | 
			
		||||
	if rqs.Continue {
 | 
			
		||||
		prefix = "CON "
 | 
			
		||||
	} else {
 | 
			
		||||
		prefix = "END "
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = io.WriteString(rqs.Writer, prefix)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return rqs, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer)
 | 
			
		||||
	return rqs, err
 | 
			
		||||
}
 | 
			
		||||
@ -1,122 +0,0 @@
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("httpserver")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DefaultRequestParser struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	v := rqv.Header.Get("X-Vise-Session")
 | 
			
		||||
	if v == "" {
 | 
			
		||||
		return "", handlers.ErrSessionMissing
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	defer rqv.Body.Close()
 | 
			
		||||
	v, err := ioutil.ReadAll(rqv.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SessionHandler struct {
 | 
			
		||||
	handlers.RequestHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
 | 
			
		||||
	return &SessionHandler{
 | 
			
		||||
		RequestHandler: h,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *SessionHandler) 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{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.Errorf("error writing error!!", "err", err, "olderr", s)
 | 
			
		||||
		w.WriteHeader(500)
 | 
			
		||||
	}
 | 
			
		||||
	return 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	var code int
 | 
			
		||||
	var err error
 | 
			
		||||
	var perr error
 | 
			
		||||
 | 
			
		||||
	rqs := handlers.RequestSession{
 | 
			
		||||
		Ctx: req.Context(),
 | 
			
		||||
		Writer: w,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rp := f.GetRequestParser()
 | 
			
		||||
	cfg := f.GetConfig()
 | 
			
		||||
	cfg.SessionId, err = rp.GetSessionId(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
 | 
			
		||||
		f.writeError(w, 400, err)
 | 
			
		||||
	}
 | 
			
		||||
	rqs.Config = cfg
 | 
			
		||||
	rqs.Input, err = rp.GetInput(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
 | 
			
		||||
		f.writeError(w, 400, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rqs, err = f.Process(rqs)
 | 
			
		||||
	switch err {
 | 
			
		||||
	case handlers.ErrStorage:
 | 
			
		||||
		code = 500
 | 
			
		||||
	case handlers.ErrEngineInit:
 | 
			
		||||
		code = 500
 | 
			
		||||
	case handlers.ErrEngineExec:
 | 
			
		||||
		code = 500
 | 
			
		||||
	default:
 | 
			
		||||
		code = 200
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if code != 200 {
 | 
			
		||||
		f.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(200)
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain")
 | 
			
		||||
	rqs, err = f.Output(rqs)
 | 
			
		||||
	rqs, perr = f.Reset(rqs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		f.writeError(w, 500, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if perr != nil {
 | 
			
		||||
		f.writeError(w, 500, perr)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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,44 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Storage struct {
 | 
			
		||||
	Persister *persist.Persister
 | 
			
		||||
	UserdataDb db.Db	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StorageProvider interface {
 | 
			
		||||
	Get(sessionId string) (*Storage, error)
 | 
			
		||||
	Put(sessionId string, storage *Storage) error
 | 
			
		||||
	Close() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SimpleStorageProvider struct {
 | 
			
		||||
	*Storage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider {
 | 
			
		||||
	pe := persist.NewPersister(stateStore)
 | 
			
		||||
	pe = pe.WithFlush()
 | 
			
		||||
	return &SimpleStorageProvider{
 | 
			
		||||
		Storage: &Storage{
 | 
			
		||||
			Persister: pe,
 | 
			
		||||
			UserdataDb: userdataStore,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *SimpleStorageProvider) Get(sessionId string) (*Storage, error) {
 | 
			
		||||
	return p.Storage, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *SimpleStorageProvider) Put(sessionId string, storage *Storage) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *SimpleStorageProvider) Close() error {
 | 
			
		||||
	return p.Storage.UserdataDb.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,36 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DataTyp uint16
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DATA_ACCOUNT DataTyp = iota
 | 
			
		||||
	DATA_ACCOUNT_CREATED
 | 
			
		||||
	DATA_TRACKING_ID
 | 
			
		||||
	DATA_PUBLIC_KEY
 | 
			
		||||
	DATA_CUSTODIAL_ID
 | 
			
		||||
	DATA_ACCOUNT_PIN
 | 
			
		||||
	DATA_ACCOUNT_STATUS
 | 
			
		||||
	DATA_FIRST_NAME
 | 
			
		||||
	DATA_FAMILY_NAME
 | 
			
		||||
	DATA_YOB
 | 
			
		||||
	DATA_LOCATION
 | 
			
		||||
	DATA_GENDER
 | 
			
		||||
	DATA_OFFERINGS
 | 
			
		||||
	DATA_RECIPIENT
 | 
			
		||||
	DATA_AMOUNT
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func typToBytes(typ DataTyp) []byte {
 | 
			
		||||
	var b [2]byte
 | 
			
		||||
	binary.BigEndian.PutUint16(b[:], uint16(typ))
 | 
			
		||||
	return b[:]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PackKey(typ DataTyp, data []byte) []byte {
 | 
			
		||||
	v := typToBytes(typ)
 | 
			
		||||
	return append(v, data...)
 | 
			
		||||
}
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DataStore interface {
 | 
			
		||||
	db.Db
 | 
			
		||||
	ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error)
 | 
			
		||||
	WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserDataStore struct {
 | 
			
		||||
	db.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadEntry retrieves an entry from the store based on the provided parameters.
 | 
			
		||||
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
 | 
			
		||||
	store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	store.SetSession(sessionId)
 | 
			
		||||
	k := PackKey(typ, []byte(sessionId))
 | 
			
		||||
	return store.Get(ctx, k)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
 | 
			
		||||
	store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	store.SetSession(sessionId)
 | 
			
		||||
	k := PackKey(typ, []byte(sessionId))
 | 
			
		||||
	return store.Put(ctx, k, value)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								request/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								request/base.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
			
		||||
package request
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/db"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/engine"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/persist"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/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.grassecon.net/grassrootseconomics/visedriver/errors"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/request"
 | 
			
		||||
	slogging "github.com/grassrootseconomics/go-vise/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = slogging.Get().With("component", "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"
 | 
			
		||||
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/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.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/engine"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/persist"
 | 
			
		||||
	"github.com/grassrootseconomics/go-vise/resource"
 | 
			
		||||
	slogging "github.com/grassrootseconomics/go-vise/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = slogging.Get().With("component", "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,18 +0,0 @@
 | 
			
		||||
# Variables to match files in the current directory
 | 
			
		||||
INPUTS = $(wildcard ./*.vis)
 | 
			
		||||
TXTS = $(wildcard ./*.txt.orig)
 | 
			
		||||
VISE_PATH := ../../go-vise
 | 
			
		||||
 | 
			
		||||
# Rule to build .bin files from .vis files
 | 
			
		||||
%.vis:
 | 
			
		||||
	go run  $(VISE_PATH)/dev/asm/main.go -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
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user