This commit is contained in:
lafetz 2025-03-26 23:50:19 +03:00
parent b250fbc77e
commit ab3f6d4313
21 changed files with 750 additions and 0 deletions

46
.air.toml Normal file
View File

@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
bin
coverage.out
coverage
.env
tmp
build

5
cmd/main.go Normal file
View File

@ -0,0 +1,5 @@
package main
func main() {
}

37
compose.db.yaml Normal file
View File

@ -0,0 +1,37 @@
services:
postgres:
image: postgres:16-alpine
ports:
- 5422:5432
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_USER=root
- POSTGRES_DB=gh
networks:
- app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root -d gh"]
interval: 5s
timeout: 3s
retries: 5
migrate:
image: migrate/migrate
volumes:
- ./db/migrations:/migrations
depends_on:
postgres:
condition: service_healthy
command:
[
"-path",
"/migrations",
"-database",
"postgresql://root:secret@postgres:5432/gh?sslmode=disable",
"up",
]
networks:
- app
networks:
app:
driver: bridge

View File

@ -0,0 +1,74 @@
-- Drop tables that depend on service_type_setting
DROP TABLE IF EXISTS service_type_setting;
-- Drop product-related tables and types
DROP TABLE IF EXISTS product;
DROP TYPE IF EXISTS tier_group;
-- Drop onboarding-related tables and types
DROP TABLE IF EXISTS verification_key;
DROP TABLE IF EXISTS onboarding_user;
DROP TYPE IF EXISTS verification_status;
DROP TYPE IF EXISTS onboarding_status;
-- Drop staff-related tables and types
DROP TABLE IF EXISTS staff_session;
DROP TABLE IF EXISTS user_agent;
DROP TABLE IF EXISTS staff;
DROP TYPE IF EXISTS password_status;
-- Drop mobile app-related tables and types
DROP TABLE IF EXISTS user_devices;
DROP TABLE IF EXISTS user_session;
DROP TABLE IF EXISTS linked_accounts;
DROP TABLE IF EXISTS users;
DROP TYPE IF EXISTS device_type;
DROP TYPE IF EXISTS registeration_type;
DROP TYPE IF EXISTS customer_group;
-- Drop linked accounts and beneficiary tables and types
DROP TABLE IF EXISTS beneficiary;
DROP TYPE IF EXISTS fund_destination;
-- Drop maker checker-related tables and types
DROP TABLE IF EXISTS workflow;
DROP TYPE IF EXISTS approval_status;
DROP TYPE IF EXISTS action_type;
DROP TYPE IF EXISTS context_type;
-- Drop authorization-related tables and types
DROP TRIGGER IF EXISTS enforce_unique_array ON policy;
DROP FUNCTION IF EXISTS check_unique_array;
DROP TABLE IF EXISTS policy;
DROP TABLE IF EXISTS roles;
DROP TYPE IF EXISTS policy_action;
DROP TYPE IF EXISTS policy_object;
-- Drop bank-related tables and types
DROP TABLE IF EXISTS bank;
DROP TABLE IF EXISTS flagged_users;
-- Drop transaction-related tables and types
DROP TABLE IF EXISTS transaction_daily;
DROP TABLE IF EXISTS system_limits;
DROP TABLE IF EXISTS transactions;
DROP TYPE IF EXISTS payment_status;
DROP TYPE IF EXISTS service_type;
DROP TYPE IF EXISTS channel;
DROP TYPE IF EXISTS transaction_category;
DROP TYPE IF EXISTS registration_type;
-- Drop branches and related tables
DROP TABLE IF EXISTS branches;
DROP TABLE IF EXISTS cities;
DROP TABLE IF EXISTS districts;
DROP TABLE IF EXISTS regions;
-- Drop activity logs
DROP TABLE IF EXISTS activity;
-- Drop ussd account and related enums
DROP TABLE IF EXISTS ussd_account;
DROP TYPE IF EXISTS ua_pin_status;
DROP TYPE IF EXISTS ua_status;
DROP TYPE IF EXISTS ua_registaration_type;

View File

@ -0,0 +1,12 @@
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone_number VARCHAR(20) UNIQUE NOT NULL,
password TEXT NOT NULL,
role VARCHAR(50) NOT NULL,
verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP ,
updated_at TIMESTAMP
);

16
db/query/user.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: CreateUser :one
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
-- name: GetUserByID :one
SELECT * FROM users WHERE id = $1;
-- name: GetAllUsers :many
SELECT * FROM users;
-- name: UpdateUser :exec
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

32
gen/db/db.go Normal file
View File

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package dbgen
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

22
gen/db/models.go Normal file
View File

@ -0,0 +1,22 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
package dbgen
import (
"github.com/jackc/pgx/v5/pgtype"
)
type User struct {
ID int64
FirstName string
LastName string
Email string
PhoneNumber string
Password string
Role string
Verified pgtype.Bool
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}

149
gen/db/user.sql.go Normal file
View File

@ -0,0 +1,149 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: user.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateUser = `-- name: CreateUser :one
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at
`
type CreateUserParams struct {
FirstName string
LastName string
Email string
PhoneNumber string
Password string
Role string
Verified pgtype.Bool
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRow(ctx, CreateUser,
arg.FirstName,
arg.LastName,
arg.Email,
arg.PhoneNumber,
arg.Password,
arg.Role,
arg.Verified,
)
var i User
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteUser = `-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteUser, id)
return err
}
const GetAllUsers = `-- name: GetAllUsers :many
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users
`
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
rows, err := q.db.Query(ctx, GetAllUsers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users WHERE id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRow(ctx, GetUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const UpdateUser = `-- name: UpdateUser :exec
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1
`
type UpdateUserParams struct {
ID int64
FirstName string
LastName string
Email string
PhoneNumber string
Password string
Role string
Verified pgtype.Bool
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser,
arg.ID,
arg.FirstName,
arg.LastName,
arg.Email,
arg.PhoneNumber,
arg.Password,
arg.Role,
arg.Verified,
)
return err
}

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module github.com/SamuelTariku/FortuneBet-Backend
go 1.24.1
require github.com/jackc/pgx/v5 v5.7.4
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

28
go.sum Normal file
View File

@ -0,0 +1,28 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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
internal/domain/auth.go Normal file
View File

@ -0,0 +1 @@
package domain

12
internal/domain/user.go Normal file
View File

@ -0,0 +1,12 @@
package domain
type User struct {
ID int64
FirstName string
LastName string
Email string
PhoneNumber string
Password string
Role string
Verified bool
}

View File

@ -0,0 +1,41 @@
package repository
import (
"context"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgxpool"
)
type Store struct {
queries *dbgen.Queries
conn *pgxpool.Pool
}
func NewStore(conn *pgxpool.Pool) *Store {
queries := dbgen.New(conn)
return &Store{
queries: queries,
conn: conn,
}
}
func OpenDB(url string) (*pgxpool.Pool, func(), error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := pgxpool.New(ctx, url)
if err != nil {
return nil, func() {}, err
}
if err := conn.Ping(ctx); err != nil {
return nil, func() {}, err
}
return conn, func() {
conn.Close()
}, nil
}

View File

@ -0,0 +1,81 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
user, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
FirstName: firstName,
LastName: lastName,
Email: email,
PhoneNumber: phoneNumber,
Password: password,
Role: role,
})
if err != nil {
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Password: user.Password,
Role: user.Role,
}, nil
}
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
user, err := s.queries.GetUserByID(ctx, id)
if err != nil {
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Password: user.Password,
Role: user.Role,
}, nil
}
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
users, err := s.queries.GetAllUsers(ctx)
if err != nil {
return nil, err
}
var result []domain.User
for _, user := range users {
result = append(result, domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Password: user.Password,
Role: user.Role,
})
}
return result, nil
}
func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
ID: id,
FirstName: firstName,
LastName: lastName,
Email: email,
PhoneNumber: phoneNumber,
Password: password,
Role: role,
})
return err
}
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
return s.queries.DeleteUser(ctx, id)
}

View File

@ -0,0 +1,15 @@
package user
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type UserStore interface {
CreateUser(ctx context.Context, CfirstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error)
GetUserByID(ctx context.Context, id int64) (domain.User, error)
GetAllUsers(ctx context.Context) ([]domain.User, error)
UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error
DeleteUser(ctx context.Context, id int64) error
}

View File

@ -0,0 +1,33 @@
package user
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service struct {
userStore UserStore
}
func NewService(userStore UserStore) *Service {
return &Service{
userStore: userStore,
}
}
func (s *Service) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
return s.userStore.CreateUser(ctx, firstName, lastName, email, phoneNumber, password, role, verified)
}
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id)
}
func (s *Service) GetAllUsers(ctx context.Context) ([]domain.User, error) {
return s.userStore.GetAllUsers(ctx)
}
func (s *Service) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
return s.userStore.UpdateUser(ctx, id, firstName, lastName, email, phoneNumber, password, role, verified)
}
func (s *Service) DeleteUser(ctx context.Context, id int64) error {
return s.userStore.DeleteUser(ctx, id)
}

View File

@ -0,0 +1,76 @@
package httpserver
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
)
type App struct {
port int
Router *http.ServeMux
logger *slog.Logger
notificationSecret string
}
func NewApp(
port int, logger *slog.Logger,
notificationSecret string,
) *App {
a := &App{
Router: http.NewServeMux(),
logger: logger,
port: port,
notificationSecret: notificationSecret,
}
a.initAppRoutes()
return a
}
func (a *App) Run() error {
srv := &http.Server{
Addr: fmt.Sprintf(":%s", strconv.Itoa(a.port)),
Handler: a.Router,
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
shutdownError := make(chan error)
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
a.logger.Info("shutting down server")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
shutdownError <- srv.Shutdown(ctx)
}()
err := srv.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
return err
}
err = <-shutdownError
if err != nil {
return err
}
a.logger.Info("server stopped")
return nil
}
func (a *App) initAppRoutes() {
// a.Router.HandleFunc("/users",)
}

32
makefile Normal file
View File

@ -0,0 +1,32 @@
include .env
.PHONY: test
test:
go test ./app
.PHONY: coverage
coverage:
mkdir -p coverage
go test -coverprofile=coverage.out ./internal/... ;
go tool cover -func=coverage.out -o coverage/coverage.txt
.PHONY: build
build:
go build -ldflags="-s" -o ./bin/web ./
.PHONY: run
run:
echo "Running Go application"; \
go run ./cmd/main.go
.PHONY: air
air:
echo "Running air"; \
air -c .air.toml
.PHONY: migrations/up
migrations/new:
@echo 'Creating migration files for DB_URL'
migrate create -seq -ext=.sql -dir=./db/migrations $(name)
.PHONY: migrations/up
migrations/up:
@echo 'Running up migrations...'
migrate -path ./db/migrations -database $(DB_URL) up
.PHONY: swagger
swagger:
swag init -g cmd/main.go

18
sqlc.yaml Normal file
View File

@ -0,0 +1,18 @@
version: "2"
sql:
- schema: "./db/migrations/"
queries: "./db/query/"
engine: "postgresql"
gen:
go:
package: "dbgen"
sql_package: "pgx/v5"
out: "./gen/db"
emit_exported_queries: true
emit_json_tags: false
overrides:
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
- db_type: "uuid"
go_type: "github.com/google/uuid.NullUUID"
nullable: true