forked from grassrootseconomics/visedriver
		
	Compare commits
	
		
			344 Commits
		
	
	
		
			data-items
			...
			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 | |||
| a84c3e0852 | |||
| 8efed966a0 | |||
| e7c4b5bca7 | |||
| ed632248c5 | |||
| 5c202741d6 | |||
| c5ebdbf85b | |||
| c4282a870e | |||
| 91cd6077ce | |||
| c0ed6fa9c8 | |||
| b420a9bba0 | |||
| a20ab79355 | |||
| e0ec15b272 | |||
| 9e998f9a29 | |||
| 1c7c0af712 | |||
| a72fb08dc8 | |||
| 944fa89b3c | |||
| 48e1b02e0e | |||
| 3e0bbe5ffe | |||
| fd586d09c3 | |||
| 
						 | 
					c2a4efde2b | ||
| 
						 | 
					c9deca1180 | ||
| 
						 | 
					6d4f3109f8 | ||
| 
						 | 
					35cf3a1cd1 | ||
| 
						 | 
					1a782c1db9 | ||
| 
						 | 
					3d2ca606ca | ||
| 584d02db29 | |||
| 5937c6bf5c | |||
| 
						 | 
					10b3083647 | ||
| 
						 | 
					bb1a846cb3 | ||
| 
						 | 
					967e53d83b | ||
| 
						 | 
					d246cdee51 | ||
| 
						 | 
					d518a76536 | ||
| 
						 | 
					6f65c33be4 | 
@ -1,13 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
!/cmd/africastalking
 | 
			
		||||
!/common
 | 
			
		||||
!/config
 | 
			
		||||
!/initializers
 | 
			
		||||
!/internal
 | 
			
		||||
!/models
 | 
			
		||||
!/remote
 | 
			
		||||
!/services
 | 
			
		||||
!/LICENSE
 | 
			
		||||
!/README.md
 | 
			
		||||
!/go.*
 | 
			
		||||
!/.env.example
 | 
			
		||||
							
								
								
									
										14
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.env.example
									
									
									
									
									
								
							@ -6,15 +6,15 @@ HOST=127.0.0.1
 | 
			
		||||
AT_ENDPOINT=/ussd/africastalking
 | 
			
		||||
 | 
			
		||||
#PostgreSQL
 | 
			
		||||
DB_HOST=localhost
 | 
			
		||||
DB_USER=postgres
 | 
			
		||||
DB_PASSWORD=strongpass
 | 
			
		||||
DB_NAME=urdt_ussd
 | 
			
		||||
DB_PORT=5432
 | 
			
		||||
DB_SSLMODE=disable
 | 
			
		||||
DB_TIMEZONE=Africa/Nairobi
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
								
							@ -1,56 +0,0 @@
 | 
			
		||||
name: release
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags:
 | 
			
		||||
      - "v*"
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  docker:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v2
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v2
 | 
			
		||||
 | 
			
		||||
      - name: Check out repo
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
 | 
			
		||||
      - name: Cache Docker layers
 | 
			
		||||
        uses: actions/cache@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/.buildx-cache
 | 
			
		||||
          key: ${{ runner.os }}-buildx-${{ github.sha }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-buildx-
 | 
			
		||||
 | 
			
		||||
      - name: Login to GHCR Docker registry
 | 
			
		||||
        uses: docker/login-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.repository_owner }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      - name: Set outputs
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV \
 | 
			
		||||
          && echo "RELEASE_SHORT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Build and push image
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          context: ./
 | 
			
		||||
          file: ./Dockerfile
 | 
			
		||||
          platforms: linux/amd64
 | 
			
		||||
          push: true
 | 
			
		||||
          build-args: |
 | 
			
		||||
            BUILD=${{ env.RELEASE_SHORT_COMMIT }}
 | 
			
		||||
          cache-from: type=local,src=/tmp/.buildx-cache
 | 
			
		||||
          cache-to: type=local,dest=/tmp/.buildx-cache
 | 
			
		||||
          tags: |
 | 
			
		||||
            ghcr.io/grassrootseconomics/urdt-ussd:latest
 | 
			
		||||
            ghcr.io/grassrootseconomics/urdt-ussd:${{ env.RELEASE_TAG }}
 | 
			
		||||
							
								
								
									
										41
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,41 +0,0 @@
 | 
			
		||||
FROM golang:1.23.0-bookworm AS build
 | 
			
		||||
 | 
			
		||||
ENV CGO_ENABLED=1
 | 
			
		||||
 | 
			
		||||
ARG BUILDPLATFORM
 | 
			
		||||
ARG TARGETPLATFORM
 | 
			
		||||
ARG BUILD=dev
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN apt update && apt install libgdbm-dev
 | 
			
		||||
RUN git clone https://git.defalsify.org/vise.git go-vise
 | 
			
		||||
 | 
			
		||||
WORKDIR /build/services/registration
 | 
			
		||||
RUN echo "Compiling go-vise files"
 | 
			
		||||
RUN make VISE_PATH=/build/go-vise -B
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
 | 
			
		||||
RUN go mod download
 | 
			
		||||
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
 | 
			
		||||
 | 
			
		||||
FROM debian:bookworm-slim
 | 
			
		||||
 | 
			
		||||
ENV DEBIAN_FRONTEND=noninteractive
 | 
			
		||||
 | 
			
		||||
RUN apt update && apt install libgdbm-dev ca-certificates -y
 | 
			
		||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
WORKDIR /service
 | 
			
		||||
 | 
			
		||||
COPY --from=build /build/ussd-africastalking .
 | 
			
		||||
COPY --from=build /build/LICENSE .
 | 
			
		||||
COPY --from=build /build/README.md .
 | 
			
		||||
COPY --from=build /build/services ./services
 | 
			
		||||
COPY --from=build /build/.env.example .
 | 
			
		||||
RUN mv .env.example .env
 | 
			
		||||
 | 
			
		||||
EXPOSE 7123
 | 
			
		||||
 | 
			
		||||
CMD ["./ussd-africastalking"]
 | 
			
		||||
							
								
								
									
										92
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								README.md
									
									
									
									
									
								
							@ -1,92 +0,0 @@
 | 
			
		||||
# URDT USSD service
 | 
			
		||||
 | 
			
		||||
This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine.
 | 
			
		||||
 | 
			
		||||
## Prerequisites
 | 
			
		||||
### 1. [go-vise](https://github.com/nolash/go-vise)
 | 
			
		||||
 | 
			
		||||
Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git`
 | 
			
		||||
```
 | 
			
		||||
git clone https://git.defalsify.org/vise.git
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Setup
 | 
			
		||||
1. Clone the ussd repo in its own directory
 | 
			
		||||
 | 
			
		||||
    ```
 | 
			
		||||
    git clone https://git.grassecon.net/urdt/ussd.git
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
2. Navigate to the project directory.
 | 
			
		||||
3. Enter the `services/registration` subfolder:
 | 
			
		||||
    ```
 | 
			
		||||
    cd services/registration
 | 
			
		||||
    ```
 | 
			
		||||
4. make the .bin files from the .vis files
 | 
			
		||||
    ```
 | 
			
		||||
    make VISE_PATH=/var/path/to/your/go-vise -B
 | 
			
		||||
    ```
 | 
			
		||||
5. Return to the project root (`cd ../..`)
 | 
			
		||||
6. Run the USSD menu 
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/main.go -session-id=0712345678
 | 
			
		||||
    ```
 | 
			
		||||
## Running the different binaries
 | 
			
		||||
1. ### CLI: 
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/main.go -session-id=0712345678
 | 
			
		||||
    ```
 | 
			
		||||
2. ### Africastalking: 
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/africastalking/main.go
 | 
			
		||||
    ```
 | 
			
		||||
3. ### Async: 
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/async/main.go
 | 
			
		||||
    ```
 | 
			
		||||
4. ### Http: 
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/http/main.go
 | 
			
		||||
    ```
 | 
			
		||||
    
 | 
			
		||||
## Flags
 | 
			
		||||
Below are the supported flags:
 | 
			
		||||
 | 
			
		||||
1. `-session-id`: 
 | 
			
		||||
    
 | 
			
		||||
    Specifies the session ID. (CLI only). 
 | 
			
		||||
    
 | 
			
		||||
    Default: `075xx2123`.
 | 
			
		||||
 | 
			
		||||
    Example:
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/main.go -session-id=0712345678
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
2. `-d`: 
 | 
			
		||||
 | 
			
		||||
    Enables engine debug output. 
 | 
			
		||||
    
 | 
			
		||||
    Default: `false`.
 | 
			
		||||
 | 
			
		||||
    Example:
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/main.go -session-id=0712345678 -d
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
3. `-db`: 
 | 
			
		||||
 | 
			
		||||
    Specifies the database type.
 | 
			
		||||
    
 | 
			
		||||
    Default: `gdbm`.
 | 
			
		||||
 | 
			
		||||
    Example:
 | 
			
		||||
    ```
 | 
			
		||||
    go run cmd/main.go -session-id=0712345678 -d -db=postgres
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    >Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file.
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
[AGPL-3.0](LICENSE).
 | 
			
		||||
@ -1,222 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/common"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
 | 
			
		||||
	build = "dev"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type atRequestParser struct{}
 | 
			
		||||
 | 
			
		||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Printf("got an invalid request:", rq)
 | 
			
		||||
		return "", handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Capture body (if any) for logging
 | 
			
		||||
	body, err := io.ReadAll(rqv.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("failed to read request body:", err)
 | 
			
		||||
		return "", fmt.Errorf("failed to read request body: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	// Reset the body for further reading
 | 
			
		||||
	rqv.Body = io.NopCloser(bytes.NewReader(body))
 | 
			
		||||
 | 
			
		||||
	// Log the body as JSON
 | 
			
		||||
	bodyLog := map[string]string{"body": string(body)}
 | 
			
		||||
	logBytes, err := json.Marshal(bodyLog)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("failed to marshal request body:", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Printf("Received request:", string(logBytes))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := rqv.ParseForm(); err != nil {
 | 
			
		||||
		log.Printf("failed to parse form data: %v", err)
 | 
			
		||||
		return "", fmt.Errorf("failed to parse form data: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	phoneNumber := rqv.FormValue("phoneNumber")
 | 
			
		||||
	if phoneNumber == "" {
 | 
			
		||||
		return "", fmt.Errorf("no phone number found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Printf("Error: %v\n", err)
 | 
			
		||||
		return "", fmt.Errorf("failed to format number")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return formattedNumber, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
 | 
			
		||||
	rqv, ok := rq.(*http.Request)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, handlers.ErrInvalidRequest
 | 
			
		||||
	}
 | 
			
		||||
	if err := rqv.ParseForm(); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to parse form data: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	text := rqv.FormValue("text")
 | 
			
		||||
 | 
			
		||||
	parts := strings.Split(text, "*")
 | 
			
		||||
	if len(parts) == 0 {
 | 
			
		||||
		return nil, fmt.Errorf("no input found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return []byte(parts[len(parts)-1]), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug 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.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer userdataStore.Close()
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	lhs.SetDataStore(&userdataStore)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accountService := remote.AccountService{}
 | 
			
		||||
	hl, err := lhs.GetHandler(&accountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := menuStorageService.GetStateStore(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
	mux := http.NewServeMux()
 | 
			
		||||
	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
 | 
			
		||||
 | 
			
		||||
	s := &http.Server{
 | 
			
		||||
		Addr:    fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
 | 
			
		||||
		Handler: mux,
 | 
			
		||||
	}
 | 
			
		||||
	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,174 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug 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.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer userdataStore.Close()
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userdataStore)
 | 
			
		||||
	accountService := remote.AccountService{}
 | 
			
		||||
 | 
			
		||||
	hl, err := lhs.GetHandler(&accountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := menuStorageService.GetStateStore(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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 {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "error in process: %v", "err", err)
 | 
			
		||||
			fmt.Errorf("error in process: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		rqs, err = sh.Output(rqs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "error in output: %v", "err", err)
 | 
			
		||||
			fmt.Errorf("error in output: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		rqs, err = sh.Reset(rqs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "error in reset: %v", "err", err)
 | 
			
		||||
			fmt.Errorf("error in reset: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		_, err = fmt.Scanln(&rqs.Input)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.ErrorCtxf(ctx, "error in input", "err", err)
 | 
			
		||||
			fmt.Errorf("error in input: %v", err)
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								cmd/http/main.go
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								cmd/http/main.go
									
									
									
									
									
								
							@ -1,141 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	httpserver "git.grassecon.net/urdt/ussd/internal/http"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var resourceDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug 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.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
 | 
			
		||||
	flag.UintVar(&size, "s", 160, "max size of output")
 | 
			
		||||
	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
 | 
			
		||||
	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		cfg.EngineDebug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer userdataStore.Close()
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userdataStore)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accountService := remote.AccountService{}
 | 
			
		||||
	hl, err := lhs.GetHandler(&accountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := menuStorageService.GetStateStore(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										118
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								cmd/main.go
									
									
									
									
									
								
							@ -1,118 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join("services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initializers.LoadEnvVariables()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	config.LoadConfig()
 | 
			
		||||
 | 
			
		||||
	var dbDir string
 | 
			
		||||
	var size uint
 | 
			
		||||
	var sessionId string
 | 
			
		||||
	var database string
 | 
			
		||||
	var engineDebug bool
 | 
			
		||||
	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
 | 
			
		||||
	flag.StringVar(&database, "db", "gdbm", "database to be used")
 | 
			
		||||
	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
 | 
			
		||||
	flag.BoolVar(&engineDebug, "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)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", database)
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(size),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
 | 
			
		||||
	err := menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userdatastore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userdatastore)
 | 
			
		||||
	lhs.SetPersister(pe)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accountService := remote.AccountService{}
 | 
			
		||||
	hl, err := lhs.GetHandler(&accountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	en := lhs.GetEngine()
 | 
			
		||||
	en = en.WithFirst(hl.Init)
 | 
			
		||||
	if engineDebug {
 | 
			
		||||
		en = en.WithDebug(nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								common/db.go
									
									
									
									
									
								
							@ -1,89 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	DATA_TEMPORARY_VALUE
 | 
			
		||||
	DATA_ACTIVE_SYM
 | 
			
		||||
	DATA_ACTIVE_BAL
 | 
			
		||||
	DATA_BLOCKED_NUMBER
 | 
			
		||||
	DATA_PUBLIC_KEY_REVERSE
 | 
			
		||||
	DATA_ACTIVE_DECIMAL
 | 
			
		||||
	DATA_ACTIVE_ADDRESS
 | 
			
		||||
	DATA_PREFIX_SYMBOLS
 | 
			
		||||
	DATA_PREFIX_BALANCES
 | 
			
		||||
	DATA_PREFIX_DECIMALS
 | 
			
		||||
	DATA_PREFIX_ADDRESSES
 | 
			
		||||
	DATA_PREFIX_TX_SENDERS
 | 
			
		||||
	DATA_PREFIX_TX_RECIPIENTS
 | 
			
		||||
	DATA_PREFIX_TX_VALUES
 | 
			
		||||
	DATA_PREFIX_TX_ADDRESSES
 | 
			
		||||
	DATA_PREFIX_TX_HASHES
 | 
			
		||||
	DATA_PREFIX_TX_DATES
 | 
			
		||||
	DATA_PREFIX_TX_SYMBOLS
 | 
			
		||||
	DATA_PREFIX_TX_DECIMALS
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("urdt-common")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func StringToDataTyp(str string) (DataTyp, error) {
 | 
			
		||||
	switch str {
 | 
			
		||||
	case "DATA_FIRST_NAME":
 | 
			
		||||
		return DATA_FIRST_NAME, nil
 | 
			
		||||
	case "DATA_FAMILY_NAME":
 | 
			
		||||
		return DATA_FAMILY_NAME, nil
 | 
			
		||||
	case "DATA_YOB":
 | 
			
		||||
		return DATA_YOB, nil
 | 
			
		||||
	case "DATA_LOCATION":
 | 
			
		||||
		return DATA_LOCATION, nil
 | 
			
		||||
	case "DATA_GENDER":
 | 
			
		||||
		return DATA_GENDER, nil
 | 
			
		||||
	case "DATA_OFFERINGS":
 | 
			
		||||
		return DATA_OFFERINGS, nil
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, errors.New("invalid DataTyp string")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Convert DataTyp to []byte
 | 
			
		||||
func (d DataTyp) ToBytes() []byte {
 | 
			
		||||
	bytes := make([]byte, 2)
 | 
			
		||||
	binary.BigEndian.PutUint16(bytes, uint16(d))
 | 
			
		||||
	return bytes
 | 
			
		||||
}
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NormalizeHex(s string) (string, error) {
 | 
			
		||||
	if len(s) >= 2 {
 | 
			
		||||
		if s[:2] == "0x" {
 | 
			
		||||
			s = s[2:]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	r, err := hex.DecodeString(s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return hex.EncodeToString(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsSameHex(left string, right string) bool {
 | 
			
		||||
	bl, err := NormalizeHex(left)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	br, err := NormalizeHex(left)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Compare(bl, br) == 0
 | 
			
		||||
}
 | 
			
		||||
@ -1,73 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Define the regex patterns as constants
 | 
			
		||||
const (
 | 
			
		||||
	phoneRegex   = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$`
 | 
			
		||||
	addressRegex = `^0x[a-fA-F0-9]{40}$`
 | 
			
		||||
	aliasRegex   = `^[a-zA-Z0-9]+$`
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IsValidPhoneNumber checks if the given number is a valid phone number
 | 
			
		||||
func IsValidPhoneNumber(phonenumber string) bool {
 | 
			
		||||
	match, _ := regexp.MatchString(phoneRegex, phonenumber)
 | 
			
		||||
	return match
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidAddress checks if the given address is a valid Ethereum address
 | 
			
		||||
func IsValidAddress(address string) bool {
 | 
			
		||||
	match, _ := regexp.MatchString(addressRegex, address)
 | 
			
		||||
	return match
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidAlias checks if the alias is a valid alias format
 | 
			
		||||
func IsValidAlias(alias string) bool {
 | 
			
		||||
	match, _ := regexp.MatchString(aliasRegex, alias)
 | 
			
		||||
	return match
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckRecipient validates the recipient format based on the criteria
 | 
			
		||||
func CheckRecipient(recipient string) (string, error) {
 | 
			
		||||
	if IsValidPhoneNumber(recipient) {
 | 
			
		||||
		return "phone number", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if IsValidAddress(recipient) {
 | 
			
		||||
		return "address", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if IsValidAlias(recipient) {
 | 
			
		||||
		return "alias", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx".
 | 
			
		||||
func FormatPhoneNumber(phone string) (string, error) {
 | 
			
		||||
	if !IsValidPhoneNumber(phone) {
 | 
			
		||||
		return "", errors.New("invalid phone number")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Remove any leading "+" and spaces
 | 
			
		||||
	phone = strings.TrimPrefix(phone, "+")
 | 
			
		||||
	phone = strings.ReplaceAll(phone, " ", "")
 | 
			
		||||
 | 
			
		||||
	// Replace leading "0" with "254" if present
 | 
			
		||||
	if strings.HasPrefix(phone, "0") {
 | 
			
		||||
		phone = "254" + phone[1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add "+" if not already present
 | 
			
		||||
	if !strings.HasPrefix(phone, "254") {
 | 
			
		||||
		return "", errors.New("unexpected format")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "+" + phone, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func StoreToDb(store *UserDataStore) db.Db {
 | 
			
		||||
	return store.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
 | 
			
		||||
	return storage.NewSubPrefixDb(store.Db, pfx)	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StorageServices interface {
 | 
			
		||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
			
		||||
	GetUserdataDb(ctx context.Context) (db.Db, error)
 | 
			
		||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
			
		||||
	EnsureDbDir() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StorageService struct {
 | 
			
		||||
	svc *storage.MenuStorageService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewStorageService(dbDir string) *StorageService {
 | 
			
		||||
	return &StorageService{
 | 
			
		||||
		svc: storage.NewMenuStorageService(dbDir, ""),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
			
		||||
	return ss.svc.GetPersister(ctx)
 | 
			
		||||
}
 | 
			
		||||
	
 | 
			
		||||
func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
 | 
			
		||||
	return ss.svc.GetUserdataDb(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	return nil, errors.New("not implemented")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(ss *StorageService) EnsureDbDir() error {
 | 
			
		||||
	return ss.svc.EnsureDbDir()
 | 
			
		||||
}
 | 
			
		||||
@ -1,81 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TransactionData struct {
 | 
			
		||||
	TemporaryValue string
 | 
			
		||||
	ActiveSym      string
 | 
			
		||||
	Amount         string
 | 
			
		||||
	PublicKey      string
 | 
			
		||||
	Recipient      string
 | 
			
		||||
	ActiveDecimal  string
 | 
			
		||||
	ActiveAddress  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) {
 | 
			
		||||
	// Parse token decimal
 | 
			
		||||
	tokenDecimal, err := strconv.Atoi(activeDecimal)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse amount
 | 
			
		||||
	amount, _, err := big.ParseFloat(storedAmount, 10, 0, big.ToZero)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Scale the amount
 | 
			
		||||
	multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimal)), nil))
 | 
			
		||||
	finalAmount := new(big.Float).Mul(amount, multiplier)
 | 
			
		||||
 | 
			
		||||
	// Convert finalAmount to a string
 | 
			
		||||
	finalAmountStr := new(big.Int)
 | 
			
		||||
	finalAmount.Int(finalAmountStr)
 | 
			
		||||
 | 
			
		||||
	return finalAmountStr.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) {
 | 
			
		||||
	data := TransactionData{}
 | 
			
		||||
	fieldToKey := map[string]DataTyp{
 | 
			
		||||
		"TemporaryValue": DATA_TEMPORARY_VALUE,
 | 
			
		||||
		"ActiveSym":      DATA_ACTIVE_SYM,
 | 
			
		||||
		"Amount":         DATA_AMOUNT,
 | 
			
		||||
		"PublicKey":      DATA_PUBLIC_KEY,
 | 
			
		||||
		"Recipient":      DATA_RECIPIENT,
 | 
			
		||||
		"ActiveDecimal":  DATA_ACTIVE_DECIMAL,
 | 
			
		||||
		"ActiveAddress":  DATA_ACTIVE_ADDRESS,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v := reflect.ValueOf(&data).Elem()
 | 
			
		||||
	for fieldName, key := range fieldToKey {
 | 
			
		||||
		field := v.FieldByName(fieldName)
 | 
			
		||||
		if !field.IsValid() || !field.CanSet() {
 | 
			
		||||
			return data, errors.New("invalid struct field: " + fieldName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		value, err := readStringEntry(ctx, store, sessionId, key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return data, err
 | 
			
		||||
		}
 | 
			
		||||
		field.SetString(value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readStringEntry(ctx context.Context, store DataStore, sessionId string, key DataTyp) (string, error) {
 | 
			
		||||
	entry, err := store.ReadEntry(ctx, sessionId, key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return string(entry), nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,129 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/alecthomas/assert/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParseAndScaleAmount(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name        string
 | 
			
		||||
		amount      string
 | 
			
		||||
		decimals    string
 | 
			
		||||
		want        string
 | 
			
		||||
		expectError bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:        "whole number",
 | 
			
		||||
			amount:      "123",
 | 
			
		||||
			decimals:    "2",
 | 
			
		||||
			want:        "12300",
 | 
			
		||||
			expectError: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "decimal number",
 | 
			
		||||
			amount:      "123.45",
 | 
			
		||||
			decimals:    "2",
 | 
			
		||||
			want:        "12345",
 | 
			
		||||
			expectError: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "zero decimals",
 | 
			
		||||
			amount:      "123.45",
 | 
			
		||||
			decimals:    "0",
 | 
			
		||||
			want:        "123",
 | 
			
		||||
			expectError: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "large number",
 | 
			
		||||
			amount:      "1000000.01",
 | 
			
		||||
			decimals:    "6",
 | 
			
		||||
			want:        "1000000010000",
 | 
			
		||||
			expectError: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "invalid amount",
 | 
			
		||||
			amount:      "abc",
 | 
			
		||||
			decimals:    "2",
 | 
			
		||||
			want:        "",
 | 
			
		||||
			expectError: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "invalid decimals",
 | 
			
		||||
			amount:      "123.45",
 | 
			
		||||
			decimals:    "abc",
 | 
			
		||||
			want:        "",
 | 
			
		||||
			expectError: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "zero amount",
 | 
			
		||||
			amount:      "0",
 | 
			
		||||
			decimals:    "2",
 | 
			
		||||
			want:        "0",
 | 
			
		||||
			expectError: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got, err := ParseAndScaleAmount(tt.amount, tt.decimals)
 | 
			
		||||
 | 
			
		||||
			// Check error cases
 | 
			
		||||
			if tt.expectError {
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					t.Errorf("ParseAndScaleAmount(%q, %q) expected error, got nil", tt.amount, tt.decimals)
 | 
			
		||||
				}
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("ParseAndScaleAmount(%q, %q) unexpected error: %v", tt.amount, tt.decimals, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if got != tt.want {
 | 
			
		||||
				t.Errorf("ParseAndScaleAmount(%q, %q) = %v, want %v", tt.amount, tt.decimals, got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestReadTransactionData(t *testing.T) {
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
	publicKey := "0X13242618721"
 | 
			
		||||
	ctx, store := InitializeTestDb(t)
 | 
			
		||||
 | 
			
		||||
	// Test transaction data
 | 
			
		||||
	transactionData := map[DataTyp]string{
 | 
			
		||||
		DATA_TEMPORARY_VALUE: "0712345678",
 | 
			
		||||
		DATA_ACTIVE_SYM:      "SRF",
 | 
			
		||||
		DATA_AMOUNT:          "1000000",
 | 
			
		||||
		DATA_PUBLIC_KEY:      publicKey,
 | 
			
		||||
		DATA_RECIPIENT:       "0x41c188d63Qa",
 | 
			
		||||
		DATA_ACTIVE_DECIMAL:  "6",
 | 
			
		||||
		DATA_ACTIVE_ADDRESS:  "0xd4c288865Ce",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Store the data
 | 
			
		||||
	for key, value := range transactionData {
 | 
			
		||||
		if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResult := TransactionData{
 | 
			
		||||
		TemporaryValue: "0712345678",
 | 
			
		||||
		ActiveSym:      "SRF",
 | 
			
		||||
		Amount:         "1000000",
 | 
			
		||||
		PublicKey:      publicKey,
 | 
			
		||||
		Recipient:      "0x41c188d63Qa",
 | 
			
		||||
		ActiveDecimal:  "6",
 | 
			
		||||
		ActiveAddress:  "0xd4c288865Ce",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data, err := ReadTransactionData(ctx, store, sessionId)
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, expectedResult, data)
 | 
			
		||||
}
 | 
			
		||||
@ -1,119 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TransferMetadata helps organize data fields
 | 
			
		||||
type TransferMetadata struct {
 | 
			
		||||
	Senders        string
 | 
			
		||||
	Recipients     string
 | 
			
		||||
	TransferValues string
 | 
			
		||||
	Addresses      string
 | 
			
		||||
	TxHashes       string
 | 
			
		||||
	Dates          string
 | 
			
		||||
	Symbols        string
 | 
			
		||||
	Decimals       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProcessTransfers converts transfers into formatted strings
 | 
			
		||||
func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata {
 | 
			
		||||
	var data TransferMetadata
 | 
			
		||||
	var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string
 | 
			
		||||
 | 
			
		||||
	for _, t := range transfers {
 | 
			
		||||
		senders = append(senders, t.Sender)
 | 
			
		||||
		recipients = append(recipients, t.Recipient)
 | 
			
		||||
 | 
			
		||||
		// Scale down the amount
 | 
			
		||||
		scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals)
 | 
			
		||||
		transferValues = append(transferValues, scaledBalance)
 | 
			
		||||
 | 
			
		||||
		addresses = append(addresses, t.ContractAddress)
 | 
			
		||||
		txHashes = append(txHashes, t.TxHash)
 | 
			
		||||
		dates = append(dates, fmt.Sprintf("%s", t.DateBlock))
 | 
			
		||||
		symbols = append(symbols, t.TokenSymbol)
 | 
			
		||||
		decimals = append(decimals, t.TokenDecimals)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data.Senders = strings.Join(senders, "\n")
 | 
			
		||||
	data.Recipients = strings.Join(recipients, "\n")
 | 
			
		||||
	data.TransferValues = strings.Join(transferValues, "\n")
 | 
			
		||||
	data.Addresses = strings.Join(addresses, "\n")
 | 
			
		||||
	data.TxHashes = strings.Join(txHashes, "\n")
 | 
			
		||||
	data.Dates = strings.Join(dates, "\n")
 | 
			
		||||
	data.Symbols = strings.Join(symbols, "\n")
 | 
			
		||||
	data.Decimals = strings.Join(decimals, "\n")
 | 
			
		||||
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTransferData retrieves and matches transfer data
 | 
			
		||||
// returns a formatted string of the full transaction/statement
 | 
			
		||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
 | 
			
		||||
	keys := []DataTyp{DATA_PREFIX_TX_SENDERS, DATA_PREFIX_TX_RECIPIENTS, DATA_PREFIX_TX_VALUES, DATA_PREFIX_TX_ADDRESSES, DATA_PREFIX_TX_HASHES, DATA_PREFIX_TX_DATES, DATA_PREFIX_TX_SYMBOLS}
 | 
			
		||||
	data := make(map[DataTyp]string)
 | 
			
		||||
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		value, err := db.Get(ctx, key.ToBytes())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("failed to get %s: %v", key.ToBytes(), err)
 | 
			
		||||
		}
 | 
			
		||||
		data[key] = string(value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Split the data
 | 
			
		||||
	senders := strings.Split(string(data[DATA_PREFIX_TX_SENDERS]), "\n")
 | 
			
		||||
	recipients := strings.Split(string(data[DATA_PREFIX_TX_RECIPIENTS]), "\n")
 | 
			
		||||
	values := strings.Split(string(data[DATA_PREFIX_TX_VALUES]), "\n")
 | 
			
		||||
	addresses := strings.Split(string(data[DATA_PREFIX_TX_ADDRESSES]), "\n")
 | 
			
		||||
	hashes := strings.Split(string(data[DATA_PREFIX_TX_HASHES]), "\n")
 | 
			
		||||
	dates := strings.Split(string(data[DATA_PREFIX_TX_DATES]), "\n")
 | 
			
		||||
	syms := strings.Split(string(data[DATA_PREFIX_TX_SYMBOLS]), "\n")
 | 
			
		||||
 | 
			
		||||
	// Check if index is within range
 | 
			
		||||
	if index < 1 || index > len(senders) {
 | 
			
		||||
		return "", fmt.Errorf("transaction not found: index %d out of range", index)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Adjust for 0-based indexing
 | 
			
		||||
	i := index - 1
 | 
			
		||||
	transactionType := "received"
 | 
			
		||||
	party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
 | 
			
		||||
	if strings.TrimSpace(senders[i]) == publicKey {
 | 
			
		||||
		transactionType = "sent"
 | 
			
		||||
		party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	formattedDate := formatDate(strings.TrimSpace(dates[i]))
 | 
			
		||||
 | 
			
		||||
	// Build the full transaction detail
 | 
			
		||||
	detail := fmt.Sprintf(
 | 
			
		||||
		"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
 | 
			
		||||
		transactionType,
 | 
			
		||||
		strings.TrimSpace(values[i]),
 | 
			
		||||
		strings.TrimSpace(syms[i]),
 | 
			
		||||
		party,
 | 
			
		||||
		strings.TrimSpace(addresses[i]),
 | 
			
		||||
		strings.TrimSpace(hashes[i]),
 | 
			
		||||
		formattedDate,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return detail, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to format date in desired output
 | 
			
		||||
func formatDate(dateStr string) string {
 | 
			
		||||
	parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println("Error parsing date:", err)
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return parsedDate.Format("2006-01-02 03:04:05 PM")
 | 
			
		||||
}
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
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 to the userdata store.
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteEntry adds an entry to the userdata store.
 | 
			
		||||
// BUG: this uses sessionId twice
 | 
			
		||||
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
 | 
			
		||||
	store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	store.SetSession(sessionId)
 | 
			
		||||
	k := PackKey(typ, []byte(sessionId))
 | 
			
		||||
	return store.Put(ctx, k, value)
 | 
			
		||||
}
 | 
			
		||||
@ -1,174 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// VoucherMetadata helps organize data fields
 | 
			
		||||
type VoucherMetadata struct {
 | 
			
		||||
	Symbols   string
 | 
			
		||||
	Balances  string
 | 
			
		||||
	Decimals  string
 | 
			
		||||
	Addresses string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProcessVouchers converts holdings into formatted strings
 | 
			
		||||
func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
 | 
			
		||||
	var data VoucherMetadata
 | 
			
		||||
	var symbols, balances, decimals, addresses []string
 | 
			
		||||
 | 
			
		||||
	for i, h := range holdings {
 | 
			
		||||
		symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
 | 
			
		||||
 | 
			
		||||
		// Scale down the balance
 | 
			
		||||
		scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals)
 | 
			
		||||
 | 
			
		||||
		balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance))
 | 
			
		||||
		decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
 | 
			
		||||
		addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data.Symbols = strings.Join(symbols, "\n")
 | 
			
		||||
	data.Balances = strings.Join(balances, "\n")
 | 
			
		||||
	data.Decimals = strings.Join(decimals, "\n")
 | 
			
		||||
	data.Addresses = strings.Join(addresses, "\n")
 | 
			
		||||
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ScaleDownBalance(balance, decimals string) string {
 | 
			
		||||
	// Convert balance and decimals to big.Float
 | 
			
		||||
	bal := new(big.Float)
 | 
			
		||||
	bal.SetString(balance)
 | 
			
		||||
 | 
			
		||||
	dec, ok := new(big.Int).SetString(decimals, 10)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil))
 | 
			
		||||
	scaledBalance := new(big.Float).Quo(bal, divisor)
 | 
			
		||||
 | 
			
		||||
	// Return the scaled balance without trailing decimals if it's an integer
 | 
			
		||||
	if scaledBalance.IsInt() {
 | 
			
		||||
		return scaledBalance.Text('f', 0)
 | 
			
		||||
	}
 | 
			
		||||
	return scaledBalance.Text('f', -1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetVoucherData retrieves and matches voucher data
 | 
			
		||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	keys := []DataTyp{DATA_PREFIX_SYMBOLS, DATA_PREFIX_BALANCES, DATA_PREFIX_DECIMALS, DATA_PREFIX_ADDRESSES}
 | 
			
		||||
	data := make(map[DataTyp]string)
 | 
			
		||||
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		value, err := db.Get(ctx, key.ToBytes())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("failed to get %s: %v", key.ToBytes(), err)	
 | 
			
		||||
		}
 | 
			
		||||
		data[key] = string(value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	symbol, balance, decimal, address := MatchVoucher(input,
 | 
			
		||||
		data[DATA_PREFIX_SYMBOLS],
 | 
			
		||||
		data[DATA_PREFIX_BALANCES],
 | 
			
		||||
		data[DATA_PREFIX_DECIMALS],
 | 
			
		||||
		data[DATA_PREFIX_ADDRESSES],
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if symbol == "" {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &dataserviceapi.TokenHoldings{
 | 
			
		||||
		TokenSymbol:     string(symbol),
 | 
			
		||||
		Balance:         string(balance),
 | 
			
		||||
		TokenDecimals:   string(decimal),
 | 
			
		||||
		ContractAddress: string(address),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
 | 
			
		||||
func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) {
 | 
			
		||||
	symList := strings.Split(symbols, "\n")
 | 
			
		||||
	balList := strings.Split(balances, "\n")
 | 
			
		||||
	decList := strings.Split(decimals, "\n")
 | 
			
		||||
	addrList := strings.Split(addresses, "\n")
 | 
			
		||||
 | 
			
		||||
	logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input)
 | 
			
		||||
	for i, sym := range symList {
 | 
			
		||||
		parts := strings.SplitN(sym, ":", 2)
 | 
			
		||||
 | 
			
		||||
		if input == parts[0] || strings.EqualFold(input, parts[1]) {
 | 
			
		||||
			symbol = parts[1]
 | 
			
		||||
			if i < len(balList) {
 | 
			
		||||
				balance = strings.SplitN(balList[i], ":", 2)[1]
 | 
			
		||||
			}
 | 
			
		||||
			if i < len(decList) {
 | 
			
		||||
				decimal = strings.SplitN(decList[i], ":", 2)[1]
 | 
			
		||||
			}
 | 
			
		||||
			if i < len(addrList) {
 | 
			
		||||
				address = strings.SplitN(addrList[i], ":", 2)[1]
 | 
			
		||||
			}
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
 | 
			
		||||
func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
 | 
			
		||||
	tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress)
 | 
			
		||||
 | 
			
		||||
	if err := store.WriteEntry(ctx, sessionId, DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
 | 
			
		||||
func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	temp_data, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	values := strings.SplitN(string(temp_data), ",", 4)
 | 
			
		||||
 | 
			
		||||
	data := &dataserviceapi.TokenHoldings{}
 | 
			
		||||
 | 
			
		||||
	data.TokenSymbol = values[0]
 | 
			
		||||
	data.Balance = values[1]
 | 
			
		||||
	data.TokenDecimals = values[2]
 | 
			
		||||
	data.ContractAddress = values[3]
 | 
			
		||||
 | 
			
		||||
	return data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateVoucherData updates the active voucher data in the DataStore.
 | 
			
		||||
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
 | 
			
		||||
	logg.TraceCtxf(ctx, "dtal", "data", data)
 | 
			
		||||
	// Active voucher data entries
 | 
			
		||||
	activeEntries := map[DataTyp][]byte{
 | 
			
		||||
		DATA_ACTIVE_SYM:     []byte(data.TokenSymbol),
 | 
			
		||||
		DATA_ACTIVE_BAL:     []byte(data.Balance),
 | 
			
		||||
		DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
 | 
			
		||||
		DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Write active data
 | 
			
		||||
	for key, value := range activeEntries {
 | 
			
		||||
		if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,197 +0,0 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/alecthomas/assert/v2"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	memdb "git.defalsify.org/vise.git/db/mem"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// InitializeTestDb sets up and returns an in-memory database and store.
 | 
			
		||||
func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	// Initialize memDb
 | 
			
		||||
	db := memdb.NewMemDb()
 | 
			
		||||
	err := db.Connect(ctx, "")
 | 
			
		||||
	require.NoError(t, err, "Failed to connect to memDb")
 | 
			
		||||
 | 
			
		||||
	// Create UserDataStore with memDb
 | 
			
		||||
	store := &UserDataStore{Db: db}
 | 
			
		||||
 | 
			
		||||
	t.Cleanup(func() {
 | 
			
		||||
		db.Close() // Ensure the DB is closed after each test
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return ctx, store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMatchVoucher(t *testing.T) {
 | 
			
		||||
	symbols := "1:SRF\n2:MILO"
 | 
			
		||||
	balances := "1:100\n2:200"
 | 
			
		||||
	decimals := "1:6\n2:4"
 | 
			
		||||
	addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa"
 | 
			
		||||
 | 
			
		||||
	// Test for valid voucher
 | 
			
		||||
	symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses)
 | 
			
		||||
 | 
			
		||||
	// Assertions for valid voucher
 | 
			
		||||
	assert.Equal(t, "MILO", symbol)
 | 
			
		||||
	assert.Equal(t, "200", balance)
 | 
			
		||||
	assert.Equal(t, "4", decimal)
 | 
			
		||||
	assert.Equal(t, "0x41c188d63Qa", address)
 | 
			
		||||
 | 
			
		||||
	// Test for non-existent voucher
 | 
			
		||||
	symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses)
 | 
			
		||||
 | 
			
		||||
	// Assertions for non-match
 | 
			
		||||
	assert.Equal(t, "", symbol)
 | 
			
		||||
	assert.Equal(t, "", balance)
 | 
			
		||||
	assert.Equal(t, "", decimal)
 | 
			
		||||
	assert.Equal(t, "", address)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestProcessVouchers(t *testing.T) {
 | 
			
		||||
	holdings := []dataserviceapi.TokenHoldings{
 | 
			
		||||
		{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"},
 | 
			
		||||
		{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResult := VoucherMetadata{
 | 
			
		||||
		Symbols:   "1:SRF\n2:MILO",
 | 
			
		||||
		Balances:  "1:100\n2:20000",
 | 
			
		||||
		Decimals:  "1:6\n2:4",
 | 
			
		||||
		Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result := ProcessVouchers(holdings)
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, expectedResult, result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetVoucherData(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	db := memdb.NewMemDb()
 | 
			
		||||
	err := db.Connect(ctx, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
 | 
			
		||||
 | 
			
		||||
	// Test voucher data
 | 
			
		||||
	mockData := map[DataTyp][]byte{
 | 
			
		||||
		DATA_PREFIX_SYMBOLS:   []byte("1:SRF\n2:MILO"),
 | 
			
		||||
		DATA_PREFIX_BALANCES:  []byte("1:100\n2:200"),
 | 
			
		||||
		DATA_PREFIX_DECIMALS:  []byte("1:6\n2:4"),
 | 
			
		||||
		DATA_PREFIX_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Put the data
 | 
			
		||||
	for key, value := range mockData {
 | 
			
		||||
		err = spdb.Put(ctx, []byte(key.ToBytes()), []byte(value))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := GetVoucherData(ctx, spdb, "1")
 | 
			
		||||
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, "SRF", result.TokenSymbol)
 | 
			
		||||
	assert.Equal(t, "100", result.Balance)
 | 
			
		||||
	assert.Equal(t, "6", result.TokenDecimals)
 | 
			
		||||
	assert.Equal(t, "0xd4c288865Ce", result.ContractAddress)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestStoreTemporaryVoucher(t *testing.T) {
 | 
			
		||||
	ctx, store := InitializeTestDb(t)
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
 | 
			
		||||
	// Test data
 | 
			
		||||
	voucherData := &dataserviceapi.TokenHoldings{
 | 
			
		||||
		TokenSymbol:     "SRF",
 | 
			
		||||
		Balance:         "200",
 | 
			
		||||
		TokenDecimals:   "6",
 | 
			
		||||
		ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Execute the function being tested
 | 
			
		||||
	err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// Verify stored data
 | 
			
		||||
	expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9")
 | 
			
		||||
 | 
			
		||||
	storedValue, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", DATA_TEMPORARY_VALUE)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetTemporaryVoucherData(t *testing.T) {
 | 
			
		||||
	ctx, store := InitializeTestDb(t)
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
 | 
			
		||||
	// Test voucher data
 | 
			
		||||
	tempData := &dataserviceapi.TokenHoldings{
 | 
			
		||||
		TokenSymbol:     "SRF",
 | 
			
		||||
		Balance:         "200",
 | 
			
		||||
		TokenDecimals:   "6",
 | 
			
		||||
		ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Store the data
 | 
			
		||||
	err := StoreTemporaryVoucher(ctx, store, sessionId, tempData)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// Execute the function being tested
 | 
			
		||||
	data, err := GetTemporaryVoucherData(ctx, store, sessionId)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.Equal(t, tempData, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUpdateVoucherData(t *testing.T) {
 | 
			
		||||
	ctx, store := InitializeTestDb(t)
 | 
			
		||||
	sessionId := "session123"
 | 
			
		||||
 | 
			
		||||
	// New voucher data
 | 
			
		||||
	newData := &dataserviceapi.TokenHoldings{
 | 
			
		||||
		TokenSymbol:     "SRF",
 | 
			
		||||
		Balance:         "200",
 | 
			
		||||
		TokenDecimals:   "6",
 | 
			
		||||
		ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Old temporary data
 | 
			
		||||
	tempData := &dataserviceapi.TokenHoldings{
 | 
			
		||||
		TokenSymbol:     "OLD",
 | 
			
		||||
		Balance:         "100",
 | 
			
		||||
		TokenDecimals:   "8",
 | 
			
		||||
		ContractAddress: "0xold",
 | 
			
		||||
	}
 | 
			
		||||
	require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData))
 | 
			
		||||
 | 
			
		||||
	// Execute update
 | 
			
		||||
	err := UpdateVoucherData(ctx, store, sessionId, newData)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// Verify active data was stored correctly
 | 
			
		||||
	activeEntries := map[DataTyp][]byte{
 | 
			
		||||
		DATA_ACTIVE_SYM:     []byte(newData.TokenSymbol),
 | 
			
		||||
		DATA_ACTIVE_BAL:     []byte(newData.Balance),
 | 
			
		||||
		DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals),
 | 
			
		||||
		DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for key, expectedValue := range activeEntries {
 | 
			
		||||
		storedValue, err := store.ReadEntry(ctx, sessionId, key)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										192
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								config/config.go
									
									
									
									
									
								
							@ -1,74 +1,164 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	createAccountPath          = "/api/v2/account/create"
 | 
			
		||||
	trackStatusPath            = "/api/track"
 | 
			
		||||
	balancePathPrefix          = "/api/account"
 | 
			
		||||
	trackPath                  = "/api/v2/account/status"
 | 
			
		||||
	tokenTransferPrefix        = "/api/v2/token/transfer"
 | 
			
		||||
	voucherHoldingsPathPrefix  = "/api/v1/holdings"
 | 
			
		||||
	voucherTransfersPathPrefix = "/api/v1/transfers/last10"
 | 
			
		||||
	voucherDataPathPrefix      = "/api/v1/token"
 | 
			
		||||
	AliasPrefix                = "api/v1/alias"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/env"
 | 
			
		||||
	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | 
			
		||||
	slogging "github.com/grassrootseconomics/go-vise/slog"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	custodialURLBase string
 | 
			
		||||
	dataURLBase      string
 | 
			
		||||
	BearerToken      string
 | 
			
		||||
	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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	CreateAccountURL    string
 | 
			
		||||
	TrackStatusURL      string
 | 
			
		||||
	BalanceURL          string
 | 
			
		||||
	TrackURL            string
 | 
			
		||||
	TokenTransferURL    string
 | 
			
		||||
	VoucherHoldingsURL  string
 | 
			
		||||
	VoucherTransfersURL string
 | 
			
		||||
	VoucherDataURL      string
 | 
			
		||||
	CheckAliasURL       string
 | 
			
		||||
)
 | 
			
		||||
type Override struct {
 | 
			
		||||
	DbConn           string
 | 
			
		||||
	DbConnMode       storage.DbMode
 | 
			
		||||
	StateConn        string
 | 
			
		||||
	StateConnMode    storage.DbMode
 | 
			
		||||
	ResourceConn     string
 | 
			
		||||
	ResourceConnMode storage.DbMode
 | 
			
		||||
	UserConn         string
 | 
			
		||||
	UserConnMode     storage.DbMode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setBase() error {
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003")
 | 
			
		||||
	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
 | 
			
		||||
	BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
 | 
			
		||||
 | 
			
		||||
	_, err = url.JoinPath(custodialURLBase, "/foo")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	_, err = url.JoinPath(dataURLBase, "/bar")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
	err := setBase()
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
	CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
 | 
			
		||||
	TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
 | 
			
		||||
	BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
 | 
			
		||||
	TrackURL, _ = url.JoinPath(custodialURLBase, trackPath)
 | 
			
		||||
	TokenTransferURL, _ = url.JoinPath(custodialURLBase, tokenTransferPrefix)
 | 
			
		||||
	VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
 | 
			
		||||
	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
 | 
			
		||||
	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
 | 
			
		||||
	CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
 | 
			
		||||
	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) {}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
url: http://localhost:7123
 | 
			
		||||
dial: "*384*96#"
 | 
			
		||||
phoneNumber: +254722123456
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
services:
 | 
			
		||||
  ussd-pg-store:
 | 
			
		||||
    image: postgres:17-alpine
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    user: postgres
 | 
			
		||||
    environment:
 | 
			
		||||
      - POSTGRES_PASSWORD=postgres
 | 
			
		||||
      - POSTGRES_USER=postgres
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql
 | 
			
		||||
      - ussd-pg:/var/lib/postgresql/data
 | 
			
		||||
    ports:
 | 
			
		||||
      - "127.0.0.1:5432:5432"
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD-SHELL", "pg_isready"]
 | 
			
		||||
      interval: 10s
 | 
			
		||||
      timeout: 5s
 | 
			
		||||
      retries: 5
 | 
			
		||||
volumes:
 | 
			
		||||
  ussd-pg:
 | 
			
		||||
    driver: local
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
CREATE DATABASE urdt_ussd;
 | 
			
		||||
@ -1,7 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "admins": [
 | 
			
		||||
    {
 | 
			
		||||
      "phonenumber" : "<replace with any admin number to test with >"
 | 
			
		||||
    }
 | 
			
		||||
   ]
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
package commands
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("adminstore")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Admin struct {
 | 
			
		||||
	PhoneNumber string `json:"phonenumber"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Admins []Admin `json:"admins"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Seed(ctx context.Context) error {
 | 
			
		||||
	var config Config
 | 
			
		||||
	adminstore, err := utils.NewAdminStore(ctx, "../admin_numbers")
 | 
			
		||||
	store := adminstore.FsStore
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer store.Close()
 | 
			
		||||
	data, err := os.ReadFile("admin_numbers.json")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := json.Unmarshal(data, &config); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, admin := range config.Admins {
 | 
			
		||||
		err := store.Put(ctx, []byte(admin.PhoneNumber), []byte("1"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Printf(logging.LVL_DEBUG, "Failed to insert admin number", admin.PhoneNumber)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/devtools/admin/commands"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	err := commands.Seed(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Failed to initialize a list of admins with error %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								initializers/load.go → env/load.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								initializers/load.go → env/load.go
									
									
									
									
										vendored
									
									
								
							@ -1,26 +1,32 @@
 | 
			
		||||
package initializers
 | 
			
		||||
package env
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func LoadEnvVariables() {
 | 
			
		||||
	err := godotenv.Load()
 | 
			
		||||
	LoadEnvVariablesPath(".")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadEnvVariablesPath(dir string) {
 | 
			
		||||
	fp := path.Join(dir, ".env")
 | 
			
		||||
	err := godotenv.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error loading .env file")
 | 
			
		||||
		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
 | 
			
		||||
	if value, exists := os.LookupEnv(key); exists {
 | 
			
		||||
		return value
 | 
			
		||||
	}
 | 
			
		||||
  return defaultVal
 | 
			
		||||
	return defaultVal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to safely convert environment variables to uint
 | 
			
		||||
							
								
								
									
										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")
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										36
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								go.mod
									
									
									
									
									
								
							@ -1,39 +1,27 @@
 | 
			
		||||
module git.grassecon.net/urdt/ussd
 | 
			
		||||
module git.grassecon.net/grassrootseconomics/visedriver
 | 
			
		||||
 | 
			
		||||
go 1.23.0
 | 
			
		||||
go 1.24
 | 
			
		||||
 | 
			
		||||
toolchain go1.24.6
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a
 | 
			
		||||
	github.com/alecthomas/assert/v2 v2.2.2
 | 
			
		||||
	github.com/gofrs/uuid v4.4.0+incompatible
 | 
			
		||||
	github.com/grassrootseconomics/eth-custodial v1.3.0-beta
 | 
			
		||||
	github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
 | 
			
		||||
	github.com/grassrootseconomics/go-vise v0.5.0
 | 
			
		||||
	github.com/jackc/pgx/v5 v5.7.5
 | 
			
		||||
	github.com/joho/godotenv v1.5.1
 | 
			
		||||
	github.com/peteole/testdata-loader v0.3.0
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.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.2-0.20180830191138-d8f796af33cc // indirect
 | 
			
		||||
	github.com/fxamacker/cbor/v2 v2.4.0 // indirect
 | 
			
		||||
	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
 | 
			
		||||
	github.com/hexops/gotextdiff v1.0.3 // indirect
 | 
			
		||||
	github.com/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/pgx/v5 v5.7.1 // indirect
 | 
			
		||||
	github.com/jackc/puddle/v2 v2.2.2 // indirect
 | 
			
		||||
	github.com/kr/text v0.2.0 // indirect
 | 
			
		||||
	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
 | 
			
		||||
	github.com/leonelquinteros/gotext v1.7.2 // indirect
 | 
			
		||||
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 | 
			
		||||
	github.com/rogpeppe/go-internal v1.13.1 // indirect
 | 
			
		||||
	github.com/stretchr/objx v0.5.2 // indirect
 | 
			
		||||
	github.com/stretchr/testify v1.9.0 // indirect
 | 
			
		||||
	github.com/x448/float16 v0.8.4 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.8.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.18.0 // indirect
 | 
			
		||||
	gopkg.in/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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								go.sum
									
									
									
									
									
								
							@ -1,74 +1,47 @@
 | 
			
		||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE=
 | 
			
		||||
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 | 
			
		||||
github.com/alecthomas/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
 | 
			
		||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
 | 
			
		||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 | 
			
		||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 | 
			
		||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
 | 
			
		||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
 | 
			
		||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
 | 
			
		||||
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0=
 | 
			
		||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
 | 
			
		||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
 | 
			
		||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 | 
			
		||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 | 
			
		||||
github.com/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.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
 | 
			
		||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
 | 
			
		||||
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/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 | 
			
		||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
			
		||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
 | 
			
		||||
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
 | 
			
		||||
github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s=
 | 
			
		||||
github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A=
 | 
			
		||||
github.com/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 | 
			
		||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 | 
			
		||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
			
		||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 | 
			
		||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 | 
			
		||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
 | 
			
		||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 | 
			
		||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 | 
			
		||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
			
		||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 | 
			
		||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 | 
			
		||||
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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 | 
			
		||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
 | 
			
		||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
 | 
			
		||||
@ -1,108 +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", "data", 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)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		f.hn.Exit()
 | 
			
		||||
	}()
 | 
			
		||||
	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
 | 
			
		||||
	en, ok := eni.(*engine.DefaultEngine)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
		rqs.Storage = nil
 | 
			
		||||
		if perr != nil {
 | 
			
		||||
			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
 | 
			
		||||
		}
 | 
			
		||||
		return rqs, ErrEngineType
 | 
			
		||||
	}
 | 
			
		||||
	en = en.WithFirst(f.hn.Init)
 | 
			
		||||
	if rqs.Config.EngineDebug {
 | 
			
		||||
		en = en.WithDebug(nil)
 | 
			
		||||
	}
 | 
			
		||||
	rqs.Engine = en
 | 
			
		||||
 | 
			
		||||
	r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage)
 | 
			
		||||
		rqs.Storage = nil
 | 
			
		||||
		if perr != nil {
 | 
			
		||||
			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr)
 | 
			
		||||
		}
 | 
			
		||||
		return rqs, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rqs.Continue = r 
 | 
			
		||||
	return rqs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	_, err = rqs.Engine.Flush(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,133 +0,0 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/asm"
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/utils"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HandlerService interface {
 | 
			
		||||
	GetHandler() (*ussd.Handlers, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
 | 
			
		||||
	flagParser := asm.NewFlagParser().WithDebug()
 | 
			
		||||
	_, err := flagParser.Load(fp)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return flagParser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LocalHandlerService struct {
 | 
			
		||||
	Parser        *asm.FlagParser
 | 
			
		||||
	DbRs          *resource.DbResource
 | 
			
		||||
	Pe            *persist.Persister
 | 
			
		||||
	UserdataStore *db.Db
 | 
			
		||||
	AdminStore    *utils.AdminStore
 | 
			
		||||
	Cfg           engine.Config
 | 
			
		||||
	Rs            resource.Resource
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) {
 | 
			
		||||
	parser, err := getParser(fp, debug)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &LocalHandlerService{
 | 
			
		||||
		Parser:     parser,
 | 
			
		||||
		DbRs:       dbResource,
 | 
			
		||||
		AdminStore: adminstore,
 | 
			
		||||
		Cfg:        cfg,
 | 
			
		||||
		Rs:         rs,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) {
 | 
			
		||||
	ls.Pe = Pe
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
 | 
			
		||||
	ls.UserdataStore = db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
 | 
			
		||||
	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList)
 | 
			
		||||
	ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
 | 
			
		||||
 | 
			
		||||
	return ussdHandlers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: enable setting of sessionId on engine init time
 | 
			
		||||
func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine {
 | 
			
		||||
	en := engine.NewEngine(ls.Cfg, ls.Rs)
 | 
			
		||||
	en = en.WithPersister(ls.Pe)
 | 
			
		||||
	return en
 | 
			
		||||
}
 | 
			
		||||
@ -1,54 +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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,92 +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)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	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 nil: // set code to 200 if no err
 | 
			
		||||
		code = 200
 | 
			
		||||
	case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType:
 | 
			
		||||
		code = 500
 | 
			
		||||
	default:
 | 
			
		||||
		code = 500
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.Flush(rqs.Ctx, rqs.Writer)
 | 
			
		||||
	return rqs, err
 | 
			
		||||
}
 | 
			
		||||
@ -1,449 +0,0 @@
 | 
			
		||||
package http
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// invalidRequestType is a custom type to test invalid request scenarios
 | 
			
		||||
type invalidRequestType struct{}
 | 
			
		||||
 | 
			
		||||
// errorReader is a helper type that always returns an error when Read is called
 | 
			
		||||
type errorReader struct{}
 | 
			
		||||
 | 
			
		||||
func (e *errorReader) Read(p []byte) (n int, err error) {
 | 
			
		||||
	return 0, errors.New("read error")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewATSessionHandler(t *testing.T) {
 | 
			
		||||
	mockHandler := &httpmocks.MockRequestHandler{}
 | 
			
		||||
	ash := NewATSessionHandler(mockHandler)
 | 
			
		||||
 | 
			
		||||
	if ash == nil {
 | 
			
		||||
		t.Fatal("NewATSessionHandler returned nil")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ash.SessionHandler == nil {
 | 
			
		||||
		t.Fatal("SessionHandler is nil")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestATSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		setupMocks     func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine)
 | 
			
		||||
		formData       url.Values
 | 
			
		||||
		expectedStatus int
 | 
			
		||||
		expectedBody   string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Successful request",
 | 
			
		||||
			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
 | 
			
		||||
				mrp.GetSessionIdFunc = func(rq any) (string, error) {
 | 
			
		||||
					req := rq.(*http.Request)
 | 
			
		||||
					return req.FormValue("phoneNumber"), nil
 | 
			
		||||
				}
 | 
			
		||||
				mrp.GetInputFunc = func(rq any) ([]byte, error) {
 | 
			
		||||
					req := rq.(*http.Request)
 | 
			
		||||
					text := req.FormValue("text")
 | 
			
		||||
					parts := strings.Split(text, "*")
 | 
			
		||||
					return []byte(parts[len(parts)-1]), nil
 | 
			
		||||
				}
 | 
			
		||||
				mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					rqs.Continue = true
 | 
			
		||||
					rqs.Engine = me
 | 
			
		||||
					return rqs, nil
 | 
			
		||||
				}
 | 
			
		||||
				mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
 | 
			
		||||
				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
 | 
			
		||||
				mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
 | 
			
		||||
				mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil }
 | 
			
		||||
				me.FlushFunc = func(context.Context, io.Writer) (int, error) { return 0, nil }
 | 
			
		||||
			},
 | 
			
		||||
			formData: url.Values{
 | 
			
		||||
				"phoneNumber": []string{"+1234567890"},
 | 
			
		||||
				"text":        []string{"1*2*3"},
 | 
			
		||||
			},
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
			expectedBody:   "CON ",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "GetSessionId error",
 | 
			
		||||
			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
 | 
			
		||||
				mrp.GetSessionIdFunc = func(rq any) (string, error) {
 | 
			
		||||
					return "", errors.New("no phone number found")
 | 
			
		||||
				}
 | 
			
		||||
				mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
 | 
			
		||||
				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
 | 
			
		||||
			},
 | 
			
		||||
			formData: url.Values{
 | 
			
		||||
				"text": []string{"1*2*3"},
 | 
			
		||||
			},
 | 
			
		||||
			expectedStatus: http.StatusBadRequest,
 | 
			
		||||
			expectedBody:   "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "GetInput error",
 | 
			
		||||
			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
 | 
			
		||||
				mrp.GetSessionIdFunc = func(rq any) (string, error) {
 | 
			
		||||
					req := rq.(*http.Request)
 | 
			
		||||
					return req.FormValue("phoneNumber"), nil
 | 
			
		||||
				}
 | 
			
		||||
				mrp.GetInputFunc = func(rq any) ([]byte, error) {
 | 
			
		||||
					return nil, errors.New("no input found")
 | 
			
		||||
				}
 | 
			
		||||
				mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
 | 
			
		||||
				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
 | 
			
		||||
			},
 | 
			
		||||
			formData: url.Values{
 | 
			
		||||
				"phoneNumber": []string{"+1234567890"},
 | 
			
		||||
			},
 | 
			
		||||
			expectedStatus: http.StatusBadRequest,
 | 
			
		||||
			expectedBody:   "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Process error",
 | 
			
		||||
			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) {
 | 
			
		||||
				mrp.GetSessionIdFunc = func(rq any) (string, error) {
 | 
			
		||||
					req := rq.(*http.Request)
 | 
			
		||||
					return req.FormValue("phoneNumber"), nil
 | 
			
		||||
				}
 | 
			
		||||
				mrp.GetInputFunc = func(rq any) ([]byte, error) {
 | 
			
		||||
					req := rq.(*http.Request)
 | 
			
		||||
					text := req.FormValue("text")
 | 
			
		||||
					parts := strings.Split(text, "*")
 | 
			
		||||
					return []byte(parts[len(parts)-1]), nil
 | 
			
		||||
				}
 | 
			
		||||
				mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rqs, handlers.ErrStorage
 | 
			
		||||
				}
 | 
			
		||||
				mh.GetConfigFunc = func() engine.Config { return engine.Config{} }
 | 
			
		||||
				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp }
 | 
			
		||||
			},
 | 
			
		||||
			formData: url.Values{
 | 
			
		||||
				"phoneNumber": []string{"+1234567890"},
 | 
			
		||||
				"text":        []string{"1*2*3"},
 | 
			
		||||
			},
 | 
			
		||||
			expectedStatus: http.StatusInternalServerError,
 | 
			
		||||
			expectedBody:   "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockHandler := &httpmocks.MockRequestHandler{}
 | 
			
		||||
			mockRequestParser := &httpmocks.MockRequestParser{}
 | 
			
		||||
			mockEngine := &httpmocks.MockEngine{}
 | 
			
		||||
			tt.setupMocks(mockHandler, mockRequestParser, mockEngine)
 | 
			
		||||
 | 
			
		||||
			ash := NewATSessionHandler(mockHandler)
 | 
			
		||||
 | 
			
		||||
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.formData.Encode()))
 | 
			
		||||
			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 | 
			
		||||
			w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
			ash.ServeHTTP(w, req)
 | 
			
		||||
 | 
			
		||||
			if w.Code != tt.expectedStatus {
 | 
			
		||||
				t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if tt.expectedBody != "" && w.Body.String() != tt.expectedBody {
 | 
			
		||||
				t.Errorf("Expected body %q, got %q", tt.expectedBody, w.Body.String())
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestATSessionHandler_Output(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		input          handlers.RequestSession
 | 
			
		||||
		expectedPrefix string
 | 
			
		||||
		expectedError  bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Continue true",
 | 
			
		||||
			input: handlers.RequestSession{
 | 
			
		||||
				Continue: true,
 | 
			
		||||
				Engine: &httpmocks.MockEngine{
 | 
			
		||||
					FlushFunc: func(context.Context, io.Writer) (int, error) {
 | 
			
		||||
						return 0, nil
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Writer: &httpmocks.MockWriter{},
 | 
			
		||||
			},
 | 
			
		||||
			expectedPrefix: "CON ",
 | 
			
		||||
			expectedError:  false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Continue false",
 | 
			
		||||
			input: handlers.RequestSession{
 | 
			
		||||
				Continue: false,
 | 
			
		||||
				Engine: &httpmocks.MockEngine{
 | 
			
		||||
					FlushFunc: func(context.Context, io.Writer) (int, error) {
 | 
			
		||||
						return 0, nil
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Writer: &httpmocks.MockWriter{},
 | 
			
		||||
			},
 | 
			
		||||
			expectedPrefix: "END ",
 | 
			
		||||
			expectedError:  false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Flush error",
 | 
			
		||||
			input: handlers.RequestSession{
 | 
			
		||||
				Continue: true,
 | 
			
		||||
				Engine: &httpmocks.MockEngine{
 | 
			
		||||
					FlushFunc: func(context.Context, io.Writer) (int, error) {
 | 
			
		||||
						return 0, errors.New("write error")
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Writer: &httpmocks.MockWriter{},
 | 
			
		||||
			},
 | 
			
		||||
			expectedPrefix: "CON ",
 | 
			
		||||
			expectedError:  true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			ash := &ATSessionHandler{}
 | 
			
		||||
			_, err := ash.Output(tt.input)
 | 
			
		||||
 | 
			
		||||
			if tt.expectedError && err == nil {
 | 
			
		||||
				t.Error("Expected an error, but got nil")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !tt.expectedError && err != nil {
 | 
			
		||||
				t.Errorf("Unexpected error: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mw := tt.input.Writer.(*httpmocks.MockWriter)
 | 
			
		||||
			if !mw.WriteStringCalled {
 | 
			
		||||
				t.Error("WriteString was not called")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if mw.WrittenString != tt.expectedPrefix {
 | 
			
		||||
				t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		sessionID      string
 | 
			
		||||
		input          []byte
 | 
			
		||||
		parserErr      error
 | 
			
		||||
		processErr     error
 | 
			
		||||
		outputErr      error
 | 
			
		||||
		resetErr       error
 | 
			
		||||
		expectedStatus int
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Success",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Missing Session ID",
 | 
			
		||||
			sessionID:      "",
 | 
			
		||||
			parserErr:      handlers.ErrSessionMissing,
 | 
			
		||||
			expectedStatus: http.StatusBadRequest,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Process Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			processErr:     handlers.ErrStorage,
 | 
			
		||||
			expectedStatus: http.StatusInternalServerError,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Output Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			outputErr:      errors.New("output error"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:           "Reset Error",
 | 
			
		||||
			sessionID:      "123",
 | 
			
		||||
			input:          []byte("test input"),
 | 
			
		||||
			resetErr:       errors.New("reset error"),
 | 
			
		||||
			expectedStatus: http.StatusOK,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockRequestParser := &httpmocks.MockRequestParser{
 | 
			
		||||
				GetSessionIdFunc: func(any) (string, error) {
 | 
			
		||||
					return tt.sessionID, tt.parserErr
 | 
			
		||||
				},
 | 
			
		||||
				GetInputFunc: func(any) ([]byte, error) {
 | 
			
		||||
					return tt.input, nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mockRequestHandler := &httpmocks.MockRequestHandler{
 | 
			
		||||
				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.processErr
 | 
			
		||||
				},
 | 
			
		||||
				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.outputErr
 | 
			
		||||
				},
 | 
			
		||||
				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
					return rs, tt.resetErr
 | 
			
		||||
				},
 | 
			
		||||
				GetRequestParserFunc: func() handlers.RequestParser {
 | 
			
		||||
					return mockRequestParser
 | 
			
		||||
				},
 | 
			
		||||
				GetConfigFunc: func() engine.Config {
 | 
			
		||||
					return engine.Config{}
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sessionHandler := ToSessionHandler(mockRequestHandler)
 | 
			
		||||
 | 
			
		||||
			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
 | 
			
		||||
			req.Header.Set("X-Vise-Session", tt.sessionID)
 | 
			
		||||
 | 
			
		||||
			rr := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
			sessionHandler.ServeHTTP(rr, req)
 | 
			
		||||
 | 
			
		||||
			if status := rr.Code; status != tt.expectedStatus {
 | 
			
		||||
				t.Errorf("handler returned wrong status code: got %v want %v",
 | 
			
		||||
					status, tt.expectedStatus)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSessionHandler_writeError(t *testing.T) {
 | 
			
		||||
	handler := &SessionHandler{}
 | 
			
		||||
	mockWriter := &httpmocks.MockWriter{}
 | 
			
		||||
	err := errors.New("test error")
 | 
			
		||||
 | 
			
		||||
	handler.writeError(mockWriter, http.StatusBadRequest, err)
 | 
			
		||||
 | 
			
		||||
	if mockWriter.WrittenString != "" {
 | 
			
		||||
		t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		request       any
 | 
			
		||||
		expectedID    string
 | 
			
		||||
		expectedError error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid Session ID",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				req := httptest.NewRequest(http.MethodPost, "/", nil)
 | 
			
		||||
				req.Header.Set("X-Vise-Session", "123456")
 | 
			
		||||
				return req
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedID:    "123456",
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Missing Session ID",
 | 
			
		||||
			request:       httptest.NewRequest(http.MethodPost, "/", nil),
 | 
			
		||||
			expectedID:    "",
 | 
			
		||||
			expectedError: handlers.ErrSessionMissing,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Invalid Request Type",
 | 
			
		||||
			request:       invalidRequestType{},
 | 
			
		||||
			expectedID:    "",
 | 
			
		||||
			expectedError: handlers.ErrInvalidRequest,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := &DefaultRequestParser{}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			id, err := parser.GetSessionId(tt.request)
 | 
			
		||||
 | 
			
		||||
			if id != tt.expectedID {
 | 
			
		||||
				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != tt.expectedError {
 | 
			
		||||
				t.Errorf("Expected error %v, got %v", tt.expectedError, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		request       any
 | 
			
		||||
		expectedInput []byte
 | 
			
		||||
		expectedError error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Valid Input",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedInput: []byte("test input"),
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Empty Input",
 | 
			
		||||
			request:       httptest.NewRequest(http.MethodPost, "/", nil),
 | 
			
		||||
			expectedInput: []byte{},
 | 
			
		||||
			expectedError: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "Invalid Request Type",
 | 
			
		||||
			request:       invalidRequestType{},
 | 
			
		||||
			expectedInput: nil,
 | 
			
		||||
			expectedError: handlers.ErrInvalidRequest,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Read Error",
 | 
			
		||||
			request: func() *http.Request {
 | 
			
		||||
				return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
 | 
			
		||||
			}(),
 | 
			
		||||
			expectedInput: nil,
 | 
			
		||||
			expectedError: errors.New("read error"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := &DefaultRequestParser{}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			input, err := parser.GetInput(tt.request)
 | 
			
		||||
 | 
			
		||||
			if !bytes.Equal(input, tt.expectedInput) {
 | 
			
		||||
				t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
 | 
			
		||||
				t.Errorf("Expected error %v, got %v", tt.expectedError, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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,121 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	gdbmdb "git.defalsify.org/vise.git/db/gdbm"
 | 
			
		||||
	"git.defalsify.org/vise.git/lang"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	dbC map[string]chan db.Db
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ThreadGdbmDb struct {
 | 
			
		||||
	db db.Db
 | 
			
		||||
	connStr string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewThreadGdbmDb() *ThreadGdbmDb {
 | 
			
		||||
	if dbC == nil {
 | 
			
		||||
		dbC = make(map[string]chan db.Db)
 | 
			
		||||
	}
 | 
			
		||||
	return &ThreadGdbmDb{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Connect(ctx context.Context, connStr string) error {
 | 
			
		||||
	var ok bool
 | 
			
		||||
	_, ok = dbC[connStr]
 | 
			
		||||
	if ok {
 | 
			
		||||
		logg.WarnCtxf(ctx, "already registered thread gdbm, skipping", "connStr", connStr)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	gdb := gdbmdb.NewGdbmDb()
 | 
			
		||||
	err := gdb.Connect(ctx, connStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	dbC[connStr] = make(chan db.Db, 1)
 | 
			
		||||
	dbC[connStr]<- gdb
 | 
			
		||||
	tdb.connStr = connStr
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) reserve() {
 | 
			
		||||
	if tdb.db == nil {
 | 
			
		||||
		tdb.db = <-dbC[tdb.connStr]
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) release() {
 | 
			
		||||
	if tdb.db == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	dbC[tdb.connStr] <- tdb.db
 | 
			
		||||
	tdb.db = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) SetPrefix(pfx uint8) {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	tdb.db.SetPrefix(pfx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) SetSession(sessionId string) {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	tdb.db.SetSession(sessionId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) SetLanguage(lng *lang.Language) {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	tdb.db.SetLanguage(lng)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Safe() bool {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	v := tdb.db.Safe()
 | 
			
		||||
	tdb.release()
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Prefix() uint8 {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	v := tdb.db.Prefix()
 | 
			
		||||
	tdb.release()
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) SetLock(typ uint8, locked bool) error {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	err := tdb.db.SetLock(typ, locked)
 | 
			
		||||
	tdb.release()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Put(ctx context.Context, key []byte, val []byte) error {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	err := tdb.db.Put(ctx, key, val)
 | 
			
		||||
	tdb.release()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	v, err := tdb.db.Get(ctx, key)
 | 
			
		||||
	tdb.release()
 | 
			
		||||
	return v, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Close() error {
 | 
			
		||||
	tdb.reserve()
 | 
			
		||||
	close(dbC[tdb.connStr])
 | 
			
		||||
	delete(dbC, tdb.connStr)
 | 
			
		||||
	err := tdb.db.Close()
 | 
			
		||||
	tdb.db = nil
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) {
 | 
			
		||||
	logg.Warnf("method not implemented for thread gdbm db")
 | 
			
		||||
	return nil, nil
 | 
			
		||||
}
 | 
			
		||||
@ -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,155 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	"git.defalsify.org/vise.git/db/postgres"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/initializers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("storage")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type StorageService interface {
 | 
			
		||||
	GetPersister(ctx context.Context) (*persist.Persister, error)
 | 
			
		||||
	GetUserdataDb(ctx context.Context) db.Db
 | 
			
		||||
	GetResource(ctx context.Context) (resource.Resource, error)
 | 
			
		||||
	EnsureDbDir() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MenuStorageService struct {
 | 
			
		||||
	dbDir         string
 | 
			
		||||
	resourceDir   string
 | 
			
		||||
	resourceStore db.Db
 | 
			
		||||
	stateStore    db.Db
 | 
			
		||||
	userDataStore db.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildConnStr() string {
 | 
			
		||||
	host := initializers.GetEnv("DB_HOST", "localhost")
 | 
			
		||||
	user := initializers.GetEnv("DB_USER", "postgres")
 | 
			
		||||
	password := initializers.GetEnv("DB_PASSWORD", "")
 | 
			
		||||
	dbName := initializers.GetEnv("DB_NAME", "")
 | 
			
		||||
	port := initializers.GetEnv("DB_PORT", "5432")
 | 
			
		||||
 | 
			
		||||
	connString := fmt.Sprintf(
 | 
			
		||||
		"postgres://%s:%s@%s:%s/%s",
 | 
			
		||||
		user, password, host, port, dbName,
 | 
			
		||||
	)
 | 
			
		||||
	logg.Debugf("pg conn string", "conn", connString)
 | 
			
		||||
 | 
			
		||||
	return connString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
 | 
			
		||||
	return &MenuStorageService{
 | 
			
		||||
		dbDir:       dbDir,
 | 
			
		||||
		resourceDir: resourceDir,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
 | 
			
		||||
	database, ok := ctx.Value("Database").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, fmt.Errorf("failed to select the database")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if existingDb != nil {
 | 
			
		||||
		return existingDb, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newDb db.Db
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	if database == "postgres" {
 | 
			
		||||
		newDb = postgres.NewPgDb()
 | 
			
		||||
		connStr := buildConnStr()
 | 
			
		||||
		err = newDb.Connect(ctx, connStr)
 | 
			
		||||
	} else {
 | 
			
		||||
		newDb = NewThreadGdbmDb()
 | 
			
		||||
		storeFile := path.Join(ms.dbDir, fileName)
 | 
			
		||||
		err = newDb.Connect(ctx, storeFile)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newDb, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
 | 
			
		||||
	stateStore, err := ms.GetStateStore(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pr := persist.NewPersister(stateStore)
 | 
			
		||||
	logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", stateStore)
 | 
			
		||||
	return pr, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
 | 
			
		||||
	if ms.userDataStore != nil {
 | 
			
		||||
		return ms.userDataStore, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userDataStore, err := ms.getOrCreateDb(ctx, ms.userDataStore, "userdata.gdbm")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ms.userDataStore = userDataStore
 | 
			
		||||
	return ms.userDataStore, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resource, error) {
 | 
			
		||||
	ms.resourceStore = fsdb.NewFsDb()
 | 
			
		||||
	err := ms.resourceStore.Connect(ctx, ms.resourceDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	rfs := resource.NewDbResource(ms.resourceStore)
 | 
			
		||||
	return rfs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) {
 | 
			
		||||
	if ms.stateStore != nil {
 | 
			
		||||
		return ms.stateStore, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stateStore, err := ms.getOrCreateDb(ctx, ms.stateStore, "state.gdbm")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ms.stateStore = stateStore
 | 
			
		||||
	return ms.stateStore, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) EnsureDbDir() error {
 | 
			
		||||
	err := os.MkdirAll(ms.dbDir, 0700)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("state dir create exited with error: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ms *MenuStorageService) Close() error {
 | 
			
		||||
	errA := ms.stateStore.Close()
 | 
			
		||||
	errB := ms.userDataStore.Close()
 | 
			
		||||
	errC := ms.resourceStore.Close()
 | 
			
		||||
	if errA != nil || errB != nil || errC != nil {
 | 
			
		||||
		return fmt.Errorf("%v %v %v", errA, errB, errC)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,43 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PrefixDb interface abstracts the database operations.
 | 
			
		||||
type PrefixDb interface {
 | 
			
		||||
	Get(ctx context.Context, key []byte) ([]byte, error)
 | 
			
		||||
	Put(ctx context.Context, key []byte, val []byte) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ PrefixDb = (*SubPrefixDb)(nil)
 | 
			
		||||
 | 
			
		||||
type SubPrefixDb struct {
 | 
			
		||||
	store db.Db
 | 
			
		||||
	pfx   []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
 | 
			
		||||
	return &SubPrefixDb{
 | 
			
		||||
		store: store,
 | 
			
		||||
		pfx:   pfx,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SubPrefixDb) toKey(k []byte) []byte {
 | 
			
		||||
	return append(s.pfx, k...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
 | 
			
		||||
	s.store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	key = s.toKey(key)
 | 
			
		||||
	return s.store.Get(ctx, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
 | 
			
		||||
	s.store.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	key = s.toKey(key)
 | 
			
		||||
	return s.store.Put(ctx, key, val)
 | 
			
		||||
}
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
package storage
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	memdb "git.defalsify.org/vise.git/db/mem"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSubPrefix(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	db := memdb.NewMemDb()
 | 
			
		||||
	err := db.Connect(ctx, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
 | 
			
		||||
	err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := sdba.Get(ctx, []byte("foo"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if !bytes.Equal(r, []byte("dipsy")) {
 | 
			
		||||
		t.Fatalf("expected 'dipsy', got %s", r)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sdbb := NewSubPrefixDb(db, []byte("lala"))
 | 
			
		||||
	r, err = sdbb.Get(ctx, []byte("foo"))
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		t.Fatal("expected not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	r, err = sdbb.Get(ctx, []byte("foo"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if !bytes.Equal(r, []byte("pu")) {
 | 
			
		||||
		t.Fatalf("expected 'pu', got %s", r)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err = sdba.Get(ctx, []byte("foo"))
 | 
			
		||||
	if !bytes.Equal(r, []byte("dipsy")) {
 | 
			
		||||
		t.Fatalf("expected 'dipsy', got %s", r)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,124 +0,0 @@
 | 
			
		||||
package testutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/storage"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
 | 
			
		||||
	testdataloader "github.com/peteole/testdata-loader"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	baseDir   = testdataloader.GetBasePath()
 | 
			
		||||
	logg      = logging.NewVanilla()
 | 
			
		||||
	scriptDir = path.Join(baseDir, "services", "registration")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	ctx = context.WithValue(ctx, "SessionId", sessionId)
 | 
			
		||||
	ctx = context.WithValue(ctx, "Database", "gdbm")
 | 
			
		||||
	pfp := path.Join(scriptDir, "pp.csv")
 | 
			
		||||
 | 
			
		||||
	var eventChannel = make(chan bool)
 | 
			
		||||
 | 
			
		||||
	cfg := engine.Config{
 | 
			
		||||
		Root:       "root",
 | 
			
		||||
		SessionId:  sessionId,
 | 
			
		||||
		OutputSize: uint32(160),
 | 
			
		||||
		FlagCount:  uint32(128),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbDir := ".test_state"
 | 
			
		||||
	resourceDir := scriptDir
 | 
			
		||||
	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
 | 
			
		||||
 | 
			
		||||
	err := menuStorageService.EnsureDbDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rs, err := menuStorageService.GetResource(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pe, err := menuStorageService.GetPersister(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userDataStore, err := menuStorageService.GetUserdataDb(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbResource, ok := rs.(*resource.DbResource)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
 | 
			
		||||
	lhs.SetDataStore(&userDataStore)
 | 
			
		||||
	lhs.SetPersister(pe)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if testtag.AccountService == nil {
 | 
			
		||||
		testtag.AccountService = &remote.AccountService{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch testtag.AccountService.(type) {
 | 
			
		||||
	case *testservice.TestAccountService:
 | 
			
		||||
		go func() {
 | 
			
		||||
			eventChannel <- false
 | 
			
		||||
		}()
 | 
			
		||||
	case *remote.AccountService:
 | 
			
		||||
		go func() {
 | 
			
		||||
			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | 
			
		||||
			eventChannel <- true
 | 
			
		||||
		}()
 | 
			
		||||
	default:
 | 
			
		||||
		panic("Unknown account service type")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hl, err := lhs.GetHandler(testtag.AccountService)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	en := lhs.GetEngine()
 | 
			
		||||
	en = en.WithFirst(hl.Init)
 | 
			
		||||
	cleanFn := func() {
 | 
			
		||||
		err := en.Finish()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Errorf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = menuStorageService.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logg.Errorf(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		logg.Infof("testengine storage closed")
 | 
			
		||||
	}
 | 
			
		||||
	return en, cleanFn, eventChannel
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
package httpmocks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.defalsify.org/vise.git/engine"
 | 
			
		||||
	"git.defalsify.org/vise.git/persist"
 | 
			
		||||
	"git.defalsify.org/vise.git/resource"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MockRequestHandler implements handlers.RequestHandler interface for testing
 | 
			
		||||
type MockRequestHandler struct {
 | 
			
		||||
	ProcessFunc          func(handlers.RequestSession) (handlers.RequestSession, error)
 | 
			
		||||
	GetConfigFunc        func() engine.Config
 | 
			
		||||
	GetEngineFunc        func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
 | 
			
		||||
	OutputFunc           func(rs handlers.RequestSession) (handlers.RequestSession, error)
 | 
			
		||||
	ResetFunc            func(rs handlers.RequestSession) (handlers.RequestSession, error)
 | 
			
		||||
	ShutdownFunc         func()
 | 
			
		||||
	GetRequestParserFunc func() handlers.RequestParser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
	return m.ProcessFunc(rqs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) GetConfig() engine.Config {
 | 
			
		||||
	return m.GetConfigFunc()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine {
 | 
			
		||||
	return m.GetEngineFunc(cfg, rs, pe)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
	return m.OutputFunc(rs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) {
 | 
			
		||||
	return m.ResetFunc(rs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) Shutdown() {
 | 
			
		||||
	m.ShutdownFunc()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser {
 | 
			
		||||
	return m.GetRequestParserFunc()
 | 
			
		||||
}
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
package mocks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/models"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
	"github.com/stretchr/testify/mock"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MockAccountService implements AccountServiceInterface for testing
 | 
			
		||||
type MockAccountService struct {
 | 
			
		||||
	mock.Mock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Get(0).(*models.AccountResult), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
 | 
			
		||||
	args := m.Called(publicKey)
 | 
			
		||||
	return args.Get(0).(*models.BalanceResult), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) {
 | 
			
		||||
	args := m.Called(trackingId)
 | 
			
		||||
	return args.Get(0).(*models.TrackStatusResult), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	args := m.Called(publicKey)
 | 
			
		||||
	return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
 | 
			
		||||
	args := m.Called(publicKey)
 | 
			
		||||
	return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
 | 
			
		||||
	args := m.Called(address)
 | 
			
		||||
	return args.Get(0).(*models.VoucherDataResult), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
 | 
			
		||||
	args := m.Called()
 | 
			
		||||
	return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
 | 
			
		||||
	args := m.Called(alias)
 | 
			
		||||
	return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
 | 
			
		||||
}
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
package testservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/models"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TestAccountService struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
 | 
			
		||||
	return &models.AccountResult{
 | 
			
		||||
		TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
 | 
			
		||||
		PublicKey:  "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
 | 
			
		||||
	balanceResponse := &models.BalanceResult{
 | 
			
		||||
		Balance: "0.003 CELO",
 | 
			
		||||
		Nonce:   json.Number("0"),
 | 
			
		||||
	}
 | 
			
		||||
	return balanceResponse, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
 | 
			
		||||
	return &models.TrackStatusResult{
 | 
			
		||||
		Active: true,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	return []dataserviceapi.TokenHoldings{
 | 
			
		||||
		dataserviceapi.TokenHoldings{
 | 
			
		||||
			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
 | 
			
		||||
			TokenSymbol:     "SRF",
 | 
			
		||||
			TokenDecimals:   "6",
 | 
			
		||||
			Balance:         "2745987",
 | 
			
		||||
		},
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
 | 
			
		||||
	return []dataserviceapi.Last10TxResponse{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
 | 
			
		||||
	return &models.VoucherDataResult{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
 | 
			
		||||
	return &models.TokenTransferResponse{
 | 
			
		||||
		TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
 | 
			
		||||
	return &dataserviceapi.AliasAddress{}, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
// +build !online
 | 
			
		||||
 | 
			
		||||
package testtag
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
	accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{}
 | 
			
		||||
)
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
// +build online
 | 
			
		||||
 | 
			
		||||
package testtag
 | 
			
		||||
 | 
			
		||||
import "git.grassecon.net/urdt/ussd/remote"
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	AccountService remote.AccountServiceInterface
 | 
			
		||||
)
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.defalsify.org/vise.git/db"
 | 
			
		||||
	fsdb "git.defalsify.org/vise.git/db/fs"
 | 
			
		||||
	"git.defalsify.org/vise.git/logging"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logg = logging.NewVanilla().WithDomain("adminstore")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AdminStore struct {
 | 
			
		||||
	ctx     context.Context
 | 
			
		||||
	FsStore db.Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) {
 | 
			
		||||
	fsStore, err := getFsStore(ctx, fileName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &AdminStore{ctx: ctx, FsStore: fsStore}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getFsStore(ctx context.Context, connectStr string) (db.Db, error) {
 | 
			
		||||
	fsStore := fsdb.NewFsDb()
 | 
			
		||||
	err := fsStore.Connect(ctx, connectStr)
 | 
			
		||||
	fsStore.SetPrefix(db.DATATYPE_USERDATA)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return fsStore, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Checks if the given sessionId is listed as an admin.
 | 
			
		||||
func (as *AdminStore) IsAdmin(sessionId string) (bool, error) {
 | 
			
		||||
	_, err := as.FsStore.Get(as.ctx, []byte(sessionId))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if db.IsNotFound(err) {
 | 
			
		||||
			logg.Printf(logging.LVL_INFO, "Returning false because session id was not found")
 | 
			
		||||
			return false, nil
 | 
			
		||||
		} else {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,56 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//IsValidYob checks if the provided yob can be considered valid 
 | 
			
		||||
func IsValidYOb(yob string) bool {
 | 
			
		||||
	currentYear := time.Now().Year()
 | 
			
		||||
	yearOfBirth, err := strconv.ParseInt(yob, 10, 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear {
 | 
			
		||||
		return true
 | 
			
		||||
	} else {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
var isoCodes = map[string]bool{
 | 
			
		||||
	"eng": true, // English
 | 
			
		||||
	"swa": true, // Swahili
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsValidISO639(code string) bool {
 | 
			
		||||
	return isoCodes[code]
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
func ConstructName(firstName, familyName, defaultValue string) string {
 | 
			
		||||
	name := defaultValue
 | 
			
		||||
	if familyName != defaultValue {
 | 
			
		||||
		if firstName != defaultValue {
 | 
			
		||||
			name = firstName + " " + familyName
 | 
			
		||||
		} else {
 | 
			
		||||
			name = familyName
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if firstName != defaultValue {
 | 
			
		||||
			name = firstName
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return name
 | 
			
		||||
}
 | 
			
		||||
@ -1,460 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "groups": [
 | 
			
		||||
        {
 | 
			
		||||
            "name": "my_account_change_pin",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "5",
 | 
			
		||||
                    "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Enter your old PIN\n\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Enter a new four number PIN:\n\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Confirm your new PIN:\n\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_language_change",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "2",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1235",
 | 
			
		||||
                    "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Select language:\n0:English\n1:Kiswahili"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_check_my_balance",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1235",
 | 
			
		||||
                    "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                    
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_check_community_balance",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "2",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1235",
 | 
			
		||||
                    "expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "{balance}\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
 | 
			
		||||
                    
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_firstname",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Enter your first names:\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "foo",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_familyname",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "2",
 | 
			
		||||
                    "expectedContent": "Enter family name:\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "bar",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
               
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_gender",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_yob",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "4",
 | 
			
		||||
                    "expectedContent": "Enter your year of birth\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1945",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_location",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "5",
 | 
			
		||||
                    "expectedContent": "Enter your location:\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "Kilifi",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_edit_offerings",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "6",
 | 
			
		||||
                    "expectedContent": "Enter the services or goods you offer: \n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "Bananas",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "menu_my_account_view_profile",
 | 
			
		||||
            "steps": [
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "3",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "7",
 | 
			
		||||
                    "expectedContent": "Please enter your PIN:"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "1234",
 | 
			
		||||
                    "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "input": "0",
 | 
			
		||||
                    "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
      
 | 
			
		||||
        
 | 
			
		||||
       
 | 
			
		||||
        
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -1,380 +0,0 @@
 | 
			
		||||
package menutraversaltest
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"os"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/internal/testutil/driver"
 | 
			
		||||
	"github.com/gofrs/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	testData      = driver.ReadData()
 | 
			
		||||
	testStore     = ".test_state"
 | 
			
		||||
	groupTestFile = "group_test.json"
 | 
			
		||||
	sessionID     string
 | 
			
		||||
	src           = rand.NewSource(42)
 | 
			
		||||
	g             = rand.New(src)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GenerateSessionId() string {
 | 
			
		||||
	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
 | 
			
		||||
	v, err := uu.NewV4()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return v.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extract the public key from the engine response
 | 
			
		||||
func extractPublicKey(response []byte) string {
 | 
			
		||||
	// Regex pattern to match the public key starting with 0x and 40 characters
 | 
			
		||||
	re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`)
 | 
			
		||||
	match := re.Find(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match)
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts the balance value from the engine response.
 | 
			
		||||
func extractBalance(response []byte) string {
 | 
			
		||||
	// Regex to match "Balance: <amount> <symbol>" followed by a newline
 | 
			
		||||
	re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
 | 
			
		||||
	match := re.FindSubmatch(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts the Maximum amount value from the engine response.
 | 
			
		||||
func extractMaxAmount(response []byte) string {
 | 
			
		||||
	// Regex to match "Maximum amount: <amount>" followed by a newline
 | 
			
		||||
	re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
 | 
			
		||||
	match := re.FindSubmatch(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match[1]) // "<amount>"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts the send amount value from the engine response.
 | 
			
		||||
func extractSendAmount(response []byte) string {
 | 
			
		||||
	// Regex to match the pattern "will receive X.XX SYM from"
 | 
			
		||||
	re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
 | 
			
		||||
	match := re.FindSubmatch(response)
 | 
			
		||||
	if match != nil {
 | 
			
		||||
		return string(match[1]) // Returns "X.XX SYM"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	sessionID = GenerateSessionId()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := os.RemoveAll(testStore); err != nil {
 | 
			
		||||
			log.Fatalf("Failed to delete state store %s: %v", testStore, err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	m.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccountCreationSuccessful(t *testing.T) {
 | 
			
		||||
	en, fn, eventChannel := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				_, err = en.Flush(ctx, w)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	<-eventChannel
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAccountRegistrationRejectTerms(t *testing.T) {
 | 
			
		||||
	// Generate a new UUID for this edge case test
 | 
			
		||||
	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
 | 
			
		||||
	v, err := uu.NewV4()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fail()
 | 
			
		||||
	}
 | 
			
		||||
	edgeCaseSessionID := v.String()
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(edgeCaseSessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMainMenuHelp(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "main_menu_help")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMainMenuQuit(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMyAccount_MyAddress(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for index, step := range group.Steps {
 | 
			
		||||
				t.Logf("step %v with input %v", index, step.Input)
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
				publicKey := extractPublicKey(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMainMenuSend(t *testing.T) {
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	sessions := testData
 | 
			
		||||
	for _, session := range sessions {
 | 
			
		||||
		groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
 | 
			
		||||
		for _, group := range groups {
 | 
			
		||||
			for _, step := range group.Steps {
 | 
			
		||||
				cont, err := en.Exec(ctx, []byte(step.Input))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if !cont {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				w := bytes.NewBuffer(nil)
 | 
			
		||||
				if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b := w.Bytes()
 | 
			
		||||
				balance := extractBalance(b)
 | 
			
		||||
				max_amount := extractMaxAmount(b)
 | 
			
		||||
				send_amount := extractSendAmount(b)
 | 
			
		||||
 | 
			
		||||
				expectedContent := []byte(step.ExpectedContent)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
 | 
			
		||||
				expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
 | 
			
		||||
 | 
			
		||||
				step.ExpectedContent = string(expectedContent)
 | 
			
		||||
				match, err := step.MatchesExpectedContent(b)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
 | 
			
		||||
				}
 | 
			
		||||
				if !match {
 | 
			
		||||
					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGroups(t *testing.T) {
 | 
			
		||||
	groups, err := driver.LoadTestGroups(groupTestFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Failed to load test groups: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	en, fn, _ := testutil.TestEngine(sessionID)
 | 
			
		||||
	defer fn()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	// Create test cases from loaded groups
 | 
			
		||||
	tests := driver.CreateTestCases(groups)
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.Name, func(t *testing.T) {
 | 
			
		||||
			cont, err := en.Exec(ctx, []byte(tt.Input))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !cont {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			w := bytes.NewBuffer(nil)
 | 
			
		||||
			if _, err := en.Flush(ctx, w); err != nil {
 | 
			
		||||
				t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
 | 
			
		||||
			}
 | 
			
		||||
			b := w.Bytes()
 | 
			
		||||
			balance := extractBalance(b)
 | 
			
		||||
 | 
			
		||||
			expectedContent := []byte(tt.ExpectedContent)
 | 
			
		||||
			expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
 | 
			
		||||
 | 
			
		||||
			tt.ExpectedContent = string(expectedContent)
 | 
			
		||||
 | 
			
		||||
			match, err := tt.MatchesExpectedContent(b)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
 | 
			
		||||
			}
 | 
			
		||||
			if !match {
 | 
			
		||||
				t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,133 +0,0 @@
 | 
			
		||||
[
 | 
			
		||||
    {
 | 
			
		||||
        "name": "session one",
 | 
			
		||||
        "groups": [
 | 
			
		||||
            {
 | 
			
		||||
                "name": "account_creation_successful",
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "0",
 | 
			
		||||
                        "expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "0",
 | 
			
		||||
                        "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1234",
 | 
			
		||||
                        "expectedContent": "Enter your four number PIN again:"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1111",
 | 
			
		||||
                        "expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "expectedContent": "Enter your four number PIN again:"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1234",
 | 
			
		||||
                        "expectedContent": "Your account is being created...Thank you for using Sarafu. Goodbye!"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "account_creation_reject_terms",
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "0",
 | 
			
		||||
                        "expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "expectedContent": "Thank you for using Sarafu. Goodbye!"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "send_with_invite",
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "000",
 | 
			
		||||
                        "expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "1",
 | 
			
		||||
                        "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "0712345678",
 | 
			
		||||
                        "expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "2",
 | 
			
		||||
                        "expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later."
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "main_menu_help",
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "4",
 | 
			
		||||
                        "expectedContent": "For more help,please call: 0757628885"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "main_menu_quit",
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "9",
 | 
			
		||||
                        "expectedContent": "Thank you for using Sarafu. Goodbye!"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "menu_my_account_my_address",
 | 
			
		||||
                "steps": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "",
 | 
			
		||||
                        "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "3",
 | 
			
		||||
                        "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "6",
 | 
			
		||||
                        "expectedContent": "Address: {public_key}\n0:Back\n9:Quit"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "input": "9",
 | 
			
		||||
                        "expectedContent": "Thank you for using Sarafu. Goodbye!"
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type AccountResult struct {
 | 
			
		||||
	PublicKey  string `json:"publicKey"`
 | 
			
		||||
	TrackingId string `json:"trackingId"`
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import "encoding/json"
 | 
			
		||||
 | 
			
		||||
type BalanceResult struct {
 | 
			
		||||
	Balance string      `json:"balance"`
 | 
			
		||||
	Nonce   json.Number `json:"nonce"`
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type TokenTransferResponse struct {
 | 
			
		||||
	TrackingId string `json:"trackingId"`
 | 
			
		||||
}
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Transaction struct {
 | 
			
		||||
	CreatedAt     time.Time   `json:"createdAt"`
 | 
			
		||||
	Status        string      `json:"status"`
 | 
			
		||||
	TransferValue json.Number `json:"transferValue"`
 | 
			
		||||
	TxHash        string      `json:"txHash"`
 | 
			
		||||
	TxType        string      `json:"txType"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TrackStatusResult struct {
 | 
			
		||||
	Active bool `json:"active"`
 | 
			
		||||
}
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type VoucherDataResult struct {
 | 
			
		||||
	TokenName      string `json:"tokenName"`
 | 
			
		||||
	TokenSymbol    string `json:"tokenSymbol"`
 | 
			
		||||
	TokenDecimals  int    `json:"tokenDecimals"`
 | 
			
		||||
	SinkAddress    string `json:"sinkAddress"`
 | 
			
		||||
	TokenCommodity string `json:"tokenCommodity"`
 | 
			
		||||
	TokenLocation  string `json:"tokenLocation"`
 | 
			
		||||
}
 | 
			
		||||
@ -1,294 +0,0 @@
 | 
			
		||||
package remote
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/config"
 | 
			
		||||
	"git.grassecon.net/urdt/ussd/models"
 | 
			
		||||
	"github.com/grassrootseconomics/eth-custodial/pkg/api"
 | 
			
		||||
	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AccountServiceInterface interface {
 | 
			
		||||
	CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error)
 | 
			
		||||
	CreateAccount(ctx context.Context) (*models.AccountResult, error)
 | 
			
		||||
	TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error)
 | 
			
		||||
	FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error)
 | 
			
		||||
	FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
 | 
			
		||||
	VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
 | 
			
		||||
	TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error)
 | 
			
		||||
	CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AccountService struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - trackingId: A unique identifier for the account.This should be obtained from a previous call to
 | 
			
		||||
//     CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
 | 
			
		||||
//     AccountResponse struct can be used here to check the account status during a transaction.
 | 
			
		||||
//
 | 
			
		||||
// Returns:
 | 
			
		||||
//   - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
 | 
			
		||||
//   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | 
			
		||||
//     If no error occurs, this will be nil
 | 
			
		||||
func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
 | 
			
		||||
	var r models.TrackStatusResult
 | 
			
		||||
 | 
			
		||||
	ep, err := url.JoinPath(config.TrackURL, publicKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", ep, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - publicKey: The public key associated with the account whose balance needs to be checked.
 | 
			
		||||
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
 | 
			
		||||
	var balanceResult models.BalanceResult
 | 
			
		||||
 | 
			
		||||
	ep, err := url.JoinPath(config.BalanceURL, publicKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", ep, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = doRequest(ctx, req, &balanceResult)
 | 
			
		||||
	return &balanceResult, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateAccount creates a new account in the custodial system.
 | 
			
		||||
// Returns:
 | 
			
		||||
//   - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
 | 
			
		||||
//     If there is an error during the request or processing, this will be nil.
 | 
			
		||||
//   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | 
			
		||||
//     If no error occurs, this will be nil.
 | 
			
		||||
func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
 | 
			
		||||
	var r models.AccountResult
 | 
			
		||||
	// Create a new request
 | 
			
		||||
	req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - publicKey: The public key associated with the account.
 | 
			
		||||
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
 | 
			
		||||
	var r struct {
 | 
			
		||||
		Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", ep, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r.Holdings, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - publicKey: The public key associated with the account.
 | 
			
		||||
func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
 | 
			
		||||
	var r struct {
 | 
			
		||||
		Transfers []dataserviceapi.Last10TxResponse `json:"transfers"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", ep, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r.Transfers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VoucherData retrieves voucher metadata from the data indexer API endpoint.
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - address: The voucher address.
 | 
			
		||||
func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
 | 
			
		||||
	var r struct {
 | 
			
		||||
		TokenDetails models.VoucherDataResult `json:"tokenDetails"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ep, err := url.JoinPath(config.VoucherDataURL, address)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", ep, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	return &r.TokenDetails, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TokenTransfer creates a new token transfer in the custodial system.
 | 
			
		||||
// Returns:
 | 
			
		||||
//   - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
 | 
			
		||||
//     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) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
 | 
			
		||||
	var r models.TokenTransferResponse
 | 
			
		||||
 | 
			
		||||
	// Create request payload
 | 
			
		||||
	payload := map[string]string{
 | 
			
		||||
		"amount":       amount,
 | 
			
		||||
		"from":         from,
 | 
			
		||||
		"to":           to,
 | 
			
		||||
		"tokenAddress": tokenAddress,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	payloadBytes, err := json.Marshal(payload)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create a new request
 | 
			
		||||
	req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &r, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckAliasAddress retrieves the address of an alias from the API endpoint.
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - alias: The alias of the user.
 | 
			
		||||
func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
 | 
			
		||||
	var r dataserviceapi.AliasAddress
 | 
			
		||||
 | 
			
		||||
	ep, err := url.JoinPath(config.CheckAliasURL, alias)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", ep, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = doRequest(ctx, req, &r)
 | 
			
		||||
	return &r, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
 | 
			
		||||
	var okResponse api.OKResponse
 | 
			
		||||
	var errResponse api.ErrResponse
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Authorization", "Bearer "+config.BearerToken)
 | 
			
		||||
	req.Header.Set("Content-Type", "application/json")
 | 
			
		||||
 | 
			
		||||
	logRequestDetails(req)
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
 | 
			
		||||
		errResponse.Description = err.Error()
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type"))
 | 
			
		||||
	body, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if resp.StatusCode >= http.StatusBadRequest {
 | 
			
		||||
		err := json.Unmarshal([]byte(body), &errResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return nil, errors.New(errResponse.Description)
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal([]byte(body), &okResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if len(okResponse.Result) == 0 {
 | 
			
		||||
		return nil, errors.New("Empty api result")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v, err := json.Marshal(okResponse.Result)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(v, &rcpt)
 | 
			
		||||
	return &okResponse, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func logRequestDetails(req *http.Request) {
 | 
			
		||||
	var bodyBytes []byte
 | 
			
		||||
	contentType := req.Header.Get("Content-Type")
 | 
			
		||||
	if req.Body != nil {
 | 
			
		||||
		bodyBytes, err := io.ReadAll(req.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Printf("Error reading request body: %s", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
 | 
			
		||||
	} else {
 | 
			
		||||
		bodyBytes = []byte("-")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
@ -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 @@
 | 
			
		||||
Something went wrong.Please try again
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
HALT
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Your account is being created...
 | 
			
		||||
@ -1,4 +0,0 @@
 | 
			
		||||
RELOAD verify_create_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,8 +0,0 @@
 | 
			
		||||
LOAD check_identifier 0
 | 
			
		||||
RELOAD check_identifier
 | 
			
		||||
MAP check_identifier
 | 
			
		||||
MOUT back 0
 | 
			
		||||
MOUT quit 9
 | 
			
		||||
HALT
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
INCMP quit 9
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Anwani:{{.check_identifier}}
 | 
			
		||||
@ -1,2 +0,0 @@
 | 
			
		||||
Maximum amount: {{.max_amount}}
 | 
			
		||||
Enter amount:
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
LOAD reset_transaction_amount 0
 | 
			
		||||
LOAD max_amount 10
 | 
			
		||||
RELOAD max_amount
 | 
			
		||||
MAP max_amount
 | 
			
		||||
MOUT back 0
 | 
			
		||||
HALT
 | 
			
		||||
LOAD validate_amount 64
 | 
			
		||||
RELOAD validate_amount
 | 
			
		||||
CATCH api_failure flag_api_call_error  1
 | 
			
		||||
CATCH invalid_amount flag_invalid_amount 1
 | 
			
		||||
INCMP _ 0
 | 
			
		||||
LOAD get_recipient 0
 | 
			
		||||
LOAD get_sender 64
 | 
			
		||||
LOAD get_amount 32
 | 
			
		||||
INCMP transaction_pin *
 | 
			
		||||
@ -1,2 +0,0 @@
 | 
			
		||||
Kiwango cha juu: {{.max_amount}}
 | 
			
		||||
Weka kiwango:
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Failed to connect to the custodial service.Please try again.
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
MOUT retry 1
 | 
			
		||||
MOUT quit 9
 | 
			
		||||
HALT
 | 
			
		||||
INCMP _ 1
 | 
			
		||||
INCMP quit 9
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Back
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Rudi
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Balances:
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
LOAD reset_account_authorized 0
 | 
			
		||||
RELOAD reset_account_authorized
 | 
			
		||||
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:
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user