forked from ebhomengo/niki
Merge branch 'develop' into mahsaaghagolzadeh/issue#289-gRPC
This commit is contained in:
commit
5fc6634c2b
|
|
@ -0,0 +1,34 @@
|
|||
root = "."
|
||||
tmp_dir = "cmd/productapp/temp"
|
||||
|
||||
[build]
|
||||
bin = "/entrypoint.sh"
|
||||
args_bin = []
|
||||
cmd = "go build -mod=mod -buildvcs=false -o ./cmd/productapp/temp/main ./cmd/productapp/"
|
||||
delay = 1000
|
||||
exclude_dir = ["vendor", "cmd/productapp/temp"]
|
||||
exclude_file = []
|
||||
exclude_regex = []
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
include_dir = []
|
||||
include_ext = ["go"]
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
poll = true
|
||||
poll_interval = 1000
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
|
@ -30,3 +30,5 @@ logs/
|
|||
mise.log
|
||||
|
||||
curl
|
||||
|
||||
cmd/**/temp/main
|
||||
124
Makefile
124
Makefile
|
|
@ -1,54 +1,64 @@
|
|||
# TODO: add commands for build and run in dev/produciton mode
|
||||
# --- Variables ---
|
||||
BINARY_NAME ?= niki
|
||||
BUILD_DIR ?= bin
|
||||
|
||||
ROOT=$(realpath $(dir $(lastword $(MAKEFILE_LIST))))
|
||||
# ====================================================================================
|
||||
# General Go Commands
|
||||
# ====================================================================================
|
||||
.PHONY: start test build clean mod-tidy lint install-linter help format swagger watch
|
||||
|
||||
.PHONY: help confirm lint test format build run docker swagger watch migrate/status migrate/new migrate/up migrate/down
|
||||
|
||||
confirm:
|
||||
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]
|
||||
|
||||
lint:
|
||||
which golangci-lint || (go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.0)
|
||||
golangci-lint run --config=$(ROOT)/.golangci.yml $(ROOT)/...
|
||||
start: build
|
||||
$(BUILD_DIR)/$(BINARY_NAME)
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
build:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
go build -o $(BUILD_DIR)/$(BINARY_NAME) main.go
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)/
|
||||
|
||||
mod-tidy:
|
||||
go mod tidy
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
install-linter:
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
format:
|
||||
@which gofumpt || (go install mvdan.cc/gofumpt@latest)
|
||||
@gofumpt -l -w $(ROOT)
|
||||
@gofumpt -l -w .
|
||||
@which gci || (go install github.com/daixiang0/gci@latest)
|
||||
@gci write $(ROOT) --skip-generated --skip-vendor
|
||||
@which golangci-lint || (go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.0)
|
||||
@gci write . --skip-generated --skip-vendor
|
||||
@golangci-lint run --fix
|
||||
|
||||
build:
|
||||
go build -o niki main.go
|
||||
|
||||
run:
|
||||
go run main.go --migrate
|
||||
|
||||
docker:
|
||||
sudo docker compose up -d
|
||||
|
||||
swagger:
|
||||
swag init
|
||||
|
||||
# Live Reload
|
||||
watch:
|
||||
@if command -v CompileDaemon > /dev/null; then \
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./niki"; \
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \
|
||||
else \
|
||||
read -p "Go's 'CompileDaemon' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
|
||||
read -p "Go's 'CompileDaemon' is not installed. Do you want to install it? [Y/n] " choice; \
|
||||
if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
|
||||
go install github.com/githubnemo/CompileDaemon@latest; \
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./niki"; \
|
||||
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \
|
||||
else \
|
||||
echo "You chose not to install CompileDaemon. Exiting..."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
# ====================================================================================
|
||||
# Database Migration Commands (legacy niki-core)
|
||||
# ====================================================================================
|
||||
.PHONY: migrate/status migrate/new migrate/up migrate/down
|
||||
|
||||
migrate/status:
|
||||
@sql-migrate status -env="production" -config=repository/mysql/dbconfig.yml
|
||||
|
||||
|
|
@ -64,3 +74,67 @@ migrate/up: confirm
|
|||
migrate/down: confirm
|
||||
@echo 'Tearing down last migration...'
|
||||
@sql-migrate down -env="production" -config=repository/mysql/dbconfig.yml -limit=1
|
||||
|
||||
confirm:
|
||||
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]
|
||||
|
||||
# ====================================================================================
|
||||
# Productapp Service Commands
|
||||
# ====================================================================================
|
||||
.PHONY: productapp-up productapp-up-logs productapp-rebuild productapp-watch
|
||||
|
||||
PRODUCTAPP_ENV = deploy/productapp/development/.env
|
||||
PRODUCTAPP_COMPOSE = deploy/productapp/development/docker-compose.yml
|
||||
|
||||
productapp-up:
|
||||
@echo "Building and starting productapp full stack..."
|
||||
docker compose --env-file $(PRODUCTAPP_ENV) -f $(PRODUCTAPP_COMPOSE) up --build -d
|
||||
@echo "productapp stack is up."
|
||||
|
||||
productapp-down:
|
||||
@echo "Building and starting productapp full stack..."
|
||||
docker compose --env-file $(PRODUCTAPP_ENV) -f $(PRODUCTAPP_COMPOSE) down
|
||||
@echo "productapp stack is up."
|
||||
|
||||
productapp-up-logs:
|
||||
@echo "Building and starting productapp full stack..."
|
||||
docker compose --env-file $(PRODUCTAPP_ENV) -f $(PRODUCTAPP_COMPOSE) up --build
|
||||
|
||||
# ====================================================================================
|
||||
# Docker Commands (legacy)
|
||||
# ====================================================================================
|
||||
.PHONY: docker
|
||||
|
||||
docker:
|
||||
docker compose up -d
|
||||
|
||||
# ====================================================================================
|
||||
# Help Target
|
||||
# ====================================================================================
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo ""
|
||||
@echo "General Go Commands:"
|
||||
@echo " start - Build and run niki-core locally"
|
||||
@echo " test - Run tests"
|
||||
@echo " build - Compile binary"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " mod-tidy - Clean up dependencies"
|
||||
@echo " format - Format code (gofumpt + gci + lint fix)"
|
||||
@echo " lint - Run linters"
|
||||
@echo " install-linter - Install golangci-lint"
|
||||
@echo " swagger - Generate swagger docs"
|
||||
@echo " watch - Live reload with CompileDaemon"
|
||||
@echo ""
|
||||
@echo "Database Migration (niki-core):"
|
||||
@echo " migrate/status - Show migration status"
|
||||
@echo " migrate/new - Create new migration (name=<name>)"
|
||||
@echo " migrate/up - Run migrations up"
|
||||
@echo " migrate/down - Rollback last migration"
|
||||
@echo ""
|
||||
@echo "Productapp Service:"
|
||||
@echo " productapp-up - Build and start full stack in background"
|
||||
@echo " productapp-up-logs - Build and start full stack with logs"
|
||||
@echo ""
|
||||
@echo "Docker (legacy):"
|
||||
@echo " docker - Start docker-compose services"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/productapp"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -25,27 +28,53 @@ var serveCmd = &cobra.Command{
|
|||
func serve() {
|
||||
log.Println("Product Service Starting...")
|
||||
|
||||
// TODO: Initialize database connection
|
||||
// TODO: Initialize service dependencies
|
||||
// TODO: Setup HTTP server with routes
|
||||
cfg := productapp.Config{
|
||||
HTTPServer: productapp.HTTPServerConfig{
|
||||
Port: getEnvInt("HTTP_PORT", 8080),
|
||||
},
|
||||
Database: mysql.Config{
|
||||
Username: getEnv("DB_USERNAME", "root"),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
Port: getEnvInt("DB_PORT", 3306),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
DBName: getEnv("DB_NAME", "niki_db"),
|
||||
},
|
||||
}
|
||||
|
||||
if p, err := strconv.Atoi(port); err == nil && p > 0 {
|
||||
cfg.HTTPServer.Port = p
|
||||
}
|
||||
|
||||
app := productapp.Setup(cfg)
|
||||
|
||||
// Setup graceful shutdown
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
log.Println("Shutting down Product Service gracefully...")
|
||||
os.Exit(0)
|
||||
app.Start()
|
||||
}()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Product Service OK!")
|
||||
})
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Printf("Product Service listening on port %s", port)
|
||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := app.Stop(ctx); err != nil {
|
||||
log.Fatalf("Server shutdown error: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Product Service stopped.")
|
||||
}
|
||||
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return defaultValue
|
||||
}
|
||||
n, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
ARG GO_IMAGE_NAME
|
||||
ARG GO_IMAGE_VERSION
|
||||
|
||||
FROM ${GO_IMAGE_NAME}:${GO_IMAGE_VERSION}
|
||||
ENV GOPROXY=https://package-mirror.liara.ir/repository/go/
|
||||
ENV GOSUMDB=off
|
||||
|
||||
WORKDIR /home/app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
RUN go install github.com/air-verse/air@latest
|
||||
|
||||
RUN printf '#!/bin/sh\n./cmd/productapp/temp/main migrate --up\nexec ./cmd/productapp/temp/main serve\n' > /entrypoint.sh && chmod +x /entrypoint.sh
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["air", "-c", "/home/app/.air/.air.productapp.toml"]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
services:
|
||||
productapp-mysql:
|
||||
image: mirror2.chabokan.net/mysql:8.0
|
||||
container_name: productapp-mysql
|
||||
restart: always
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- productapp-mysql-data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: niki_db
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
productapp-mysql-data:
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
services:
|
||||
productapp-app:
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: deploy/productapp/development/Dockerfile
|
||||
args:
|
||||
GO_IMAGE_NAME: ${GO_IMAGE_NAME}
|
||||
GO_IMAGE_VERSION: ${GO_IMAGE_VERSION}
|
||||
container_name: productapp-app
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../../..:/home/app
|
||||
environment:
|
||||
DB_HOST: productapp-mysql
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: secret
|
||||
DB_NAME: niki_db
|
||||
MIGRATION_PATH: /home/app/productapp/repository/migrations
|
||||
depends_on:
|
||||
productapp-mysql:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
productapp-mysql:
|
||||
image: mirror2.chabokan.net/mysql:8.0
|
||||
container_name: productapp-mysql
|
||||
restart: always
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- productapp-mysql-data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: niki_db
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
productapp-mysql-data:
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package mysql
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
}
|
||||
|
||||
func New(db *mysql.DB) *DB {
|
||||
return &DB{conn: db}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package mysql
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package service
|
||||
|
||||
type Service struct {
|
||||
repo Repo
|
||||
}
|
||||
|
||||
type Repo interface {
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type Transaction struct {
|
||||
ID uint64
|
||||
UserID uint64
|
||||
Amount float64
|
||||
Currency Currency
|
||||
ActionType TransactionType
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type TransactionType string
|
||||
|
||||
const (
|
||||
TransactionTypeDeposit TransactionType = "deposit"
|
||||
TransactionTypeWithdraw TransactionType = "withdraw"
|
||||
TransactionTypeRefund TransactionType = "refund"
|
||||
TransactionTypeDonate TransactionType = "donate"
|
||||
)
|
||||
|
||||
type Currency string
|
||||
|
||||
const (
|
||||
IRR Currency = "IRR"
|
||||
USD Currency = "USD"
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type Wallet struct {
|
||||
ID uint64
|
||||
UserID uint64 // user unique ID
|
||||
Balance float64
|
||||
Currency Currency
|
||||
UpdatedAt time.Time
|
||||
Status WalletStatus // "active", "frozen", "closed"
|
||||
}
|
||||
|
||||
type WalletStatus string
|
||||
|
||||
const (
|
||||
Frozen WalletStatus = "frozen" // when need to check , approve ,validate , solve sth (but deposit is possible)
|
||||
Active WalletStatus = "active" // when everything is ok
|
||||
|
||||
// ??
|
||||
// Closed WalletStatus = "closed" // when need to check , approve ,validate , solve sth (exp : security problem)
|
||||
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package param
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
|
||||
)
|
||||
|
||||
type CreateTransactionRequest struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency entity.Currency `json:"currency"`
|
||||
ActionType entity.TransactionType `json:"action_type"`
|
||||
}
|
||||
|
||||
type InsertTransactionResponse struct {
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package param
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
|
||||
)
|
||||
|
||||
type TransactionRequest struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
}
|
||||
|
||||
type TransactionResponse struct {
|
||||
Transaction []TransactionInfo `json:"transaction"`
|
||||
}
|
||||
|
||||
type TransactionInfo struct {
|
||||
ID uint64 `json:"id"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency entity.Currency `json:"currency"`
|
||||
ActionType entity.TransactionType `json:"action_type"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package param
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
|
||||
)
|
||||
|
||||
type WalletRequest struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
}
|
||||
|
||||
type WalletResponse struct {
|
||||
Wallet WalletInfo `json:"wallet"`
|
||||
}
|
||||
|
||||
type WalletInfo struct {
|
||||
Balance float64 `json:"balance"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Status entity.WalletStatus `json:"status"`
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package postgres
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/pkg/database/postgres"
|
||||
|
||||
type DB struct {
|
||||
conn *postgres.DB
|
||||
}
|
||||
|
||||
func New(conn *postgres.DB) *DB {
|
||||
return &DB{conn: conn}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
production:
|
||||
dialect: postgres
|
||||
datasource: "host=127.0.0.1 port=5432 user=wallet password=wallet2123 dbname=wallet_db sslmode=disable"
|
||||
dir: domain/wallet/repository/postgres/migrations
|
||||
table: wallet_migrationsns
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/param"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
)
|
||||
|
||||
func (s Service) CreateTransaction(ctx context.Context, request param.CreateTransactionRequest) (param.InsertTransactionResponse, error) {
|
||||
|
||||
const op = richerror.Op("wallet.service.CreateTransaction")
|
||||
|
||||
transaction := entity.Transaction{
|
||||
ID: 0,
|
||||
UserID: request.UserID,
|
||||
Amount: request.Amount,
|
||||
Currency: request.Currency,
|
||||
ActionType: request.ActionType,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
err := s.repo.InsertTransaction(ctx, transaction)
|
||||
if err != nil {
|
||||
return param.InsertTransactionResponse{}, err
|
||||
}
|
||||
|
||||
return param.InsertTransactionResponse{}, nil
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetTransactionListByUserID(ctx context.Context, UserID uint64) ([]entity.Transaction, error)
|
||||
GetWalletByUserID(ctx context.Context, UserID uint64) (entity.Wallet, error)
|
||||
InsertTransaction(ctx context.Context, transaction entity.Transaction) error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func New(repo Repository, cfg Config) Service {
|
||||
|
||||
return Service{repo: repo, cfg: cfg}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/param"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
)
|
||||
|
||||
func (s Service) GetUserTransactionHistory(ctx context.Context, request param.TransactionRequest) (param.TransactionResponse, error) {
|
||||
const op = richerror.Op("wallet.service.GetUserTransactionHistory")
|
||||
|
||||
transactionList, err := s.repo.GetTransactionListByUserID(ctx, request.UserID)
|
||||
|
||||
if err != nil {
|
||||
return param.TransactionResponse{}, err
|
||||
}
|
||||
|
||||
return param.TransactionResponse{Transaction: transactionEntityToTransactionInfo(transactionList)}, nil
|
||||
|
||||
}
|
||||
|
||||
func transactionEntityToTransactionInfo(TransactionList []entity.Transaction) []param.TransactionInfo {
|
||||
transactionInfoList := make([]param.TransactionInfo, len(TransactionList))
|
||||
for i, transaction := range TransactionList {
|
||||
transactionInfoList[i] = param.TransactionInfo{
|
||||
ID: transaction.ID,
|
||||
UserID: transaction.UserID,
|
||||
Amount: transaction.Amount,
|
||||
Currency: transaction.Currency,
|
||||
ActionType: transaction.ActionType,
|
||||
Timestamp: transaction.Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
return transactionInfoList
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/wallet/param"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
)
|
||||
|
||||
func (s Service) GetUserWallet(ctx context.Context, request param.WalletRequest) (param.WalletResponse, error) {
|
||||
const op = richerror.Op("wallet.service.GetUserWallet")
|
||||
|
||||
wallet, err := s.repo.GetWalletByUserID(ctx, request.UserID)
|
||||
|
||||
if err != nil {
|
||||
return param.WalletResponse{}, err
|
||||
}
|
||||
|
||||
return param.WalletResponse{
|
||||
Wallet: param.WalletInfo{
|
||||
Balance: wallet.Balance,
|
||||
UpdatedAt: wallet.UpdatedAt,
|
||||
Status: wallet.Status,
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package gamificationapp
|
||||
|
||||
type Application struct {
|
||||
Config Config
|
||||
}
|
||||
|
||||
func setUp(cnf Config) *Application {
|
||||
return &Application{
|
||||
cnf,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package gamificationapp
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package gamification
|
||||
|
||||
import (
|
||||
gamification "git.gocasts.ir/ebhomengo/niki/domain/gamification/service"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
GamificationSvc gamification.Service
|
||||
}
|
||||
|
||||
func New(gamificationSvc gamification.Service) *Handler {
|
||||
return &Handler{
|
||||
GamificationSvc: gamificationSvc,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package gamification
|
||||
|
||||
import "github.com/labstack/echo/v4"
|
||||
|
||||
func (h Handler) SetRoutes(e *echo.Echo) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package http
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/gamificationapp/delivery/http/gamification"
|
||||
|
||||
type Server struct {
|
||||
gamificationHandler *gamification.Handler
|
||||
}
|
||||
|
||||
func New(handler gamification.Handler) *Server {
|
||||
return &Server{
|
||||
gamificationHandler: &handler,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
querier "git.gocasts.ir/ebhomengo/niki/pkg/query_transaction/sql"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string `koanf:"host"`
|
||||
Port int `koanf:"port"`
|
||||
User string `koanf:"user"`
|
||||
Password string `koanf:"password"`
|
||||
DbName string `koanf:"dbName"`
|
||||
SSLMode string `koanf:"sslMode"`
|
||||
MaxIdleConn int `koanf:"maxIdleConns"`
|
||||
MaxOpenConn int `koanf:"maxOpenConns"`
|
||||
ConnMaxLifetime int `koanf:"connMaxLifetime"`
|
||||
PathOfMigrations string `koanf:"pathOfMigrations"`
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
config Config
|
||||
db *querier.SQLDB
|
||||
mu sync.Mutex
|
||||
statements map[statementKey]*sql.Stmt
|
||||
}
|
||||
|
||||
func (db *DB) Conn() *querier.SQLDB {
|
||||
return db.db
|
||||
}
|
||||
|
||||
func New(config Config) *DB {
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
config.Host, config.Port, config.User, config.Password, config.DbName, config.SSLMode)
|
||||
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't open postgres db: %w", err))
|
||||
}
|
||||
|
||||
maxIdle := config.MaxIdleConn
|
||||
|
||||
maxOpen := config.MaxOpenConn
|
||||
|
||||
lifetime := time.Duration(config.ConnMaxLifetime) * time.Second
|
||||
|
||||
db.SetMaxIdleConns(maxIdle)
|
||||
db.SetMaxOpenConns(maxOpen)
|
||||
db.SetConnMaxLifetime(lifetime)
|
||||
|
||||
return &DB{
|
||||
config: config,
|
||||
db: &querier.SQLDB{DB: db},
|
||||
statements: make(map[statementKey]*sql.Stmt),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) PrepareStatement(ctx context.Context, key statementKey, query string) (*sql.Stmt, error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
if stmt, ok := db.statements[key]; ok {
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
stmt, err := db.db.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare statement %q: %w", key, err)
|
||||
}
|
||||
db.statements[key] = stmt
|
||||
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (db *DB) CloseStatements() error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
var lastErr error
|
||||
for key, stmt := range db.statements {
|
||||
if err := stmt.Close(); err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
delete(db.statements, key)
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (db *DB) Close() error {
|
||||
return db.db.DB.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package migrator
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package postgres
|
||||
|
||||
type statementKey uint
|
||||
|
||||
const (
|
||||
StatementKeyAWalletGetTransactionHistory statementKey = iota + 1
|
||||
StatementKeyWalletInsertTransaction
|
||||
StatementKeyWalletGetUserWallet
|
||||
)
|
||||
|
|
@ -1,6 +1,23 @@
|
|||
package errmsg
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"` // General error message
|
||||
Errors map[string]interface{} `json:"errors,omitempty"` // Additional detail of error
|
||||
InternalErrCode string `json:"internal_error_code,omitempty"` // Custom error code (optional)
|
||||
}
|
||||
|
||||
func (e ErrorResponse) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
const (
|
||||
ErrValidationFailed = "input validation failed"
|
||||
ErrUnexpectedError = "unexpected error occurred"
|
||||
ErrInvalidRequestFormat = "invalid request format"
|
||||
ErrGetUserInfo = "get user info failed"
|
||||
ErrFailedDecodeBase64 = "decode data to base 64 failed"
|
||||
ErrFailedUnmarshalJson = "unmarshal data to JSON failed"
|
||||
ErrUnauthorized = "unauthorized"
|
||||
ErrorMsgAdminNotAllowed = "admin is not allowed"
|
||||
ErrorMsgNotFound = "record not found"
|
||||
ErrorMsgSomethingWentWrong = "something went wrong"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,69 @@
|
|||
package productapp
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.gocasts.ir/ebhomengo/niki/productapp/service/product"
|
||||
"log"
|
||||
|
||||
httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
|
||||
producthttp "git.gocasts.ir/ebhomengo/niki/productapp/delivery/http"
|
||||
productdb "git.gocasts.ir/ebhomengo/niki/productapp/repository/database"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
type HTTPServerConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
HTTPServer HTTPServerConfig
|
||||
Database mysql.Config
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Config Config
|
||||
HTTPServer *http.Server
|
||||
HTTPServer *producthttp.Server
|
||||
productSvc product.Service
|
||||
DB *productdb.DB
|
||||
Echo *echo.Echo
|
||||
}
|
||||
|
||||
func Setup(cfg Config) *Application {
|
||||
db := mysql.New(cfg.Database)
|
||||
productDB := productdb.New(db)
|
||||
|
||||
e := echo.New()
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
hsrv := &httpserver.Server{
|
||||
Router: e,
|
||||
}
|
||||
|
||||
productSvc := product.New(productDB)
|
||||
|
||||
srv := producthttp.NewServer(hsrv, productSvc)
|
||||
srv.RegisterRoutes()
|
||||
|
||||
return &Application{
|
||||
Config: cfg,
|
||||
productSvc: productSvc,
|
||||
HTTPServer: srv,
|
||||
DB: productDB,
|
||||
Echo: e,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) Start() {
|
||||
address := fmt.Sprintf(":%d", app.Config.HTTPServer.Port)
|
||||
log.Printf("Product Service listening on %s", address)
|
||||
if err := app.Echo.Start(address); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) Stop(ctx context.Context) error {
|
||||
return app.Echo.Shutdown(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1 @@
|
|||
package productapp
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,38 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
|
||||
"git.gocasts.ir/ebhomengo/niki/productapp/service/product"
|
||||
"github.com/labstack/echo/v4"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{}
|
||||
type Handler struct {
|
||||
productService product.Service
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (h Handler) HealthCheck(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
func NewHandler(productService product.Service) *Handler {
|
||||
return &Handler{
|
||||
productService: productService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) getProductList(c echo.Context) error {
|
||||
var req product.GetProductListRequest
|
||||
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, errmsg.ErrorResponse{
|
||||
Message: errmsg.ErrInvalidRequestFormat,
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.productService.GetProducts(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
// todo handle validation error
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (h Handler) HealthCheck(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "healthy"})
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package http
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
package http
|
||||
|
||||
import httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
|
||||
import (
|
||||
httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
|
||||
"git.gocasts.ir/ebhomengo/niki/productapp/service/product"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
HTTPServer *httpserver.Server
|
||||
Handler *Handler
|
||||
}
|
||||
|
||||
func NewServer(httpserver *httpserver.Server) *Server {
|
||||
func NewServer(httpserver *httpserver.Server, productSvc product.Service) *Server {
|
||||
return &Server{
|
||||
HTTPServer: httpserver,
|
||||
Handler: NewHandler(),
|
||||
Handler: NewHandler(productSvc),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -20,4 +23,12 @@ func (s *Server) Serve() {
|
|||
|
||||
func (s *Server) Stop() {}
|
||||
|
||||
func (s *Server) RegisterRoutes() {}
|
||||
func (s *Server) RegisterRoutes() {
|
||||
r := s.HTTPServer.Router
|
||||
r.GET("health-check", s.Handler.HealthCheck)
|
||||
|
||||
productGroup := r.Group("/v1/products")
|
||||
|
||||
productGroup.GET("", s.Handler.getProductList)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package database
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
import (
|
||||
"context"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/productapp/service/product"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
|
|
@ -11,3 +16,42 @@ func New(conn *mysql.DB) *DB {
|
|||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) GetProducts(ctx context.Context, page, pageSize uint) ([]product.Product, error) {
|
||||
const Op = "domain.repository.mysql.order.get-shipping"
|
||||
|
||||
query := "SELECT * FROM products ORDER BY id DESC limit ? offset ?"
|
||||
rows, err := db.conn.Conn().Query(query, page, pageSize)
|
||||
|
||||
if rows.Err() != nil {
|
||||
return []product.Product{}, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var products []product.Product
|
||||
|
||||
for rows.Next() {
|
||||
var s product.Product
|
||||
|
||||
err := rows.Scan(
|
||||
&s.ID,
|
||||
&s.Name,
|
||||
&s.Slug,
|
||||
&s.Description,
|
||||
&s.Price,
|
||||
&s.Stock,
|
||||
&s.IsActive,
|
||||
&s.Features,
|
||||
&s.CreatedAt,
|
||||
&s.DeletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
products = append(products, s)
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ CREATE TABLE `products` (
|
|||
`is_active` BOOLEAN DEFAULT TRUE,
|
||||
`features` JSON DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`deleted_at` TIMESTAMP DEFAULT NULL,
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
|
||||
`deleted_at` TIMESTAMP DEFAULT NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ import (
|
|||
)
|
||||
|
||||
type Product struct {
|
||||
ID uint
|
||||
Name string
|
||||
Slug string
|
||||
Description string
|
||||
Price float64
|
||||
Stock int
|
||||
IsActive bool
|
||||
Features string
|
||||
CreatedAt time.Time
|
||||
DeletedAt sql.NullTime
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `json:"price"`
|
||||
Stock int `json:"stock"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Features string `json:"features"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt sql.NullTime `json:"-"`
|
||||
}
|
||||
|
||||
type ProductImage struct {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package product
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrFailedGetProductList = errors.New("failed to get products list")
|
||||
ErrFailedCreateProduct = errors.New("failed to create product")
|
||||
ErrFailedFindProduct = errors.New("failed to find product")
|
||||
ErrCantAccessToUpdateProduct = errors.New("you are not allowed to edit this product")
|
||||
ErrFailedUpdateProduct = errors.New("failed to update product")
|
||||
ErrFailedToPreparedStatement = errors.New("failed to prepared statement: %v")
|
||||
ErrCantAccess = errors.New("you are not allowed to this action")
|
||||
)
|
||||
|
|
@ -1 +1,12 @@
|
|||
package product
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/param"
|
||||
|
||||
type GetProductListRequest struct {
|
||||
param.PaginationRequest
|
||||
}
|
||||
|
||||
type GetProductListResponse struct {
|
||||
Data []Product `json:"data"`
|
||||
param.PaginationResponse
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,38 @@
|
|||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetProducts(ctx context.Context, page, pageSize uint) ([]Product, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repository Repository
|
||||
}
|
||||
|
||||
func New(repo Repository) Service {
|
||||
return Service{
|
||||
repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Service) GetProducts(ctx context.Context, req GetProductListRequest) (GetProductListResponse, error) {
|
||||
|
||||
page := req.GetPageNumber()
|
||||
pageSize := req.PageSize
|
||||
|
||||
products, err := s.repository.GetProducts(ctx, page, pageSize)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return GetProductListResponse{}, errmsg.ErrorResponse{
|
||||
Message: ErrFailedGetProductList.Error(),
|
||||
Errors: map[string]interface{}{"repository_error": err.Error()},
|
||||
}
|
||||
}
|
||||
|
||||
return GetProductListResponse{Data: products}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package walletapp
|
||||
|
||||
type Application struct {
|
||||
}
|
||||
|
||||
func SetUp() {
|
||||
|
||||
}
|
||||
|
||||
func (app Application) StartServer() {}
|
||||
|
||||
func (app Application) StopServer() {}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package walletapp
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/repository"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
|
||||
logger "git.gocasts.ir/ebhomengo/niki/pkg/logger"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Redis redis.Config `koanf:"redis" json:"redis"`
|
||||
Repo repository.Config `koanf:"repo" json:"repo"`
|
||||
HTTPServer httpserver.Config `koanf:"http_server" json:"http_server"`
|
||||
Logger logger.Config `koanf:"logger" json:"logger"`
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package httpserver
|
||||
|
|
@ -0,0 +1 @@
|
|||
package httpserver
|
||||
|
|
@ -0,0 +1 @@
|
|||
package transaction
|
||||
Loading…
Reference in New Issue