Merge remote-tracking branch 'origin/develop' into feat/shopping-basket

This commit is contained in:
mzfarshad 2026-05-05 22:43:39 +03:30
commit 017459d6a2
428 changed files with 81607 additions and 246 deletions

34
.air/.air.productapp.toml Normal file
View File

@ -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

2
.gitignore vendored
View File

@ -30,3 +30,5 @@ logs/
mise.log
curl
cmd/**/temp/main

View File

@ -19,7 +19,7 @@ FROM alpine:3.20 AS runtime
# Copy the binary from the builder stage
COPY --from=builder /niki/niki .
# Copy migration files
# Copy migrations files
COPY --from=builder /niki/repository/mysql/migration ./repository/mysql/migration
# Expose application port

124
Makefile
View File

@ -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"

44
accountapp/app.go Normal file
View File

@ -0,0 +1,44 @@
package accountapp
import (
"log"
"git.gocasts.ir/ebhomengo/niki/accountapp/delivery/grpc"
"git.gocasts.ir/ebhomengo/niki/adapter/kavenegar"
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/domain/account/repository/mysql"
redisRepo "git.gocasts.ir/ebhomengo/niki/domain/account/repository/redis"
"git.gocasts.ir/ebhomengo/niki/domain/account/service"
database "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
rpcPkg "git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type Application struct {
GrpcServer grpc.Server
Config Config
accountSvc service.Service
}
func Setup(cfg Config, db *database.DB) Application {
redisConn := redis.New(cfg.Redis)
otpRepo := redisRepo.NewRepositoryOtp(redisConn)
mysqlRepo := mysql.New(db)
smsAdapter := kavenegar.New(cfg.Kavenegar)
accountSvc := service.NewService(cfg.accountSvc, otpRepo, mysqlRepo, smsAdapter)
rpcServer := rpcPkg.New(cfg.grpcServerCfg)
return Application{
accountSvc: accountSvc,
Config: cfg,
GrpcServer: grpc.New(rpcServer, accountSvc),
}
}
func (app *Application) Start() {
err := app.GrpcServer.Start()
if err != nil {
log.Fatalf("error in serving GRPC server: %v", err)
}
}

19
accountapp/config.go Normal file
View File

@ -0,0 +1,19 @@
package accountapp
import (
"git.gocasts.ir/ebhomengo/niki/adapter/kavenegar"
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/domain/account/service"
"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
"git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type Config struct {
accountSvc service.Config `koanf:"service"`
Redis redis.Config `koanf:"redis_db"`
MysqlDB mysql.Config `koanf:"mysql_db"`
Kavenegar kavenegar.Config `koanf:"kavenegar"`
grpcServerCfg grpc.Config `koanf:"grpc_server"`
grpcClientCfg grpc.Client `koanf:"grpc_client"`
PathOfMigration string `koanf:"path_of_migration"`
}

View File

@ -0,0 +1,68 @@
package grpc
import (
"context"
"fmt"
"log"
"net"
pb "git.gocasts.ir/ebhomengo/niki/contract/goprotobuf/account"
"git.gocasts.ir/ebhomengo/niki/domain/account/service"
"git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type Server struct {
server *grpc.RPCServer
accountSvc service.Service
pb.UnimplementedAccountServiceServer
}
func New(server *grpc.RPCServer, accountSvc service.Service) Server {
return Server{
server: server,
accountSvc: accountSvc,
}
}
func (s Server) SendOtp(ctx context.Context, req *pb.SendOtpRequest) (*pb.SendOtpResponse, error) {
err := s.accountSvc.SendOTP(ctx, req.PhoneNumber)
if err != nil {
return nil, err
}
return &pb.SendOtpResponse{}, nil
}
func (s Server) LoginOrRegister(ctx context.Context, req *pb.LoginOrRegisterRequest) (*pb.LoginOrRegisterResponse, error) {
res := &pb.LoginOrRegisterResponse{}
driver, err := s.accountSvc.LoginOrRegisterDriver(ctx, req.PhoneNumber, req.VerifyCode)
if err != nil {
return nil, err
}
id := uint64(driver.ID)
res.Id = id
res.PhoneNumber = driver.PhoneNumber
return res, nil
}
func (s Server) Start() error {
listener, err := net.Listen(s.server.Config.NetworkType, fmt.Sprintf(":%d", s.server.Config.Port))
if err != nil {
return err
}
accountSvcServer := Server{}
pb.RegisterAccountServiceServer(s.server.Server, &accountSvcServer)
if err := s.server.Server.Serve(listener); err != nil {
log.Fatalf("failed to serve: %v", err)
}
return nil
}

53
adapter/account/client.go Normal file
View File

@ -0,0 +1,53 @@
package account
import (
"context"
pb "git.gocasts.ir/ebhomengo/niki/contract/goprotobuf/account"
"git.gocasts.ir/ebhomengo/niki/driverapp/service"
"git.gocasts.ir/ebhomengo/niki/pkg/types"
"google.golang.org/grpc"
)
type Client struct {
Conn *grpc.ClientConn
}
func New(conn *grpc.ClientConn) *Client {
return &Client{
Conn: conn,
}
}
func (c Client) SendOTP(ctx context.Context, phoneNumber string) error {
client := pb.NewAccountServiceClient(c.Conn)
_, err := client.SendOtp(ctx, &pb.SendOtpRequest{
PhoneNumber: phoneNumber,
})
if err != nil {
return err
}
return nil
}
func (c Client) LoginOrRegister(ctx context.Context, req service.LoginOrRegisterRequest) (service.LoginOrRegisterResponse, error) {
client := pb.NewAccountServiceClient(c.Conn)
res, err := client.LoginOrRegister(ctx, &pb.LoginOrRegisterRequest{
PhoneNumber: req.PhoneNumber,
VerifyCode: req.VerifyCode,
})
if err != nil {
return service.LoginOrRegisterResponse{}, err
}
return service.LoginOrRegisterResponse{
ID: types.ID(res.Id),
PhoneNumber: res.PhoneNumber,
}, nil
}

View File

@ -1,7 +0,0 @@
package agentapp
type Application struct {
config Config
}
func Setup() {}

View File

@ -1,7 +0,0 @@
package agentapp
type Config struct {
// database config
// httpserver config
//...
}

View File

@ -1 +0,0 @@
package http

View File

@ -1,14 +0,0 @@
package http
type Server struct {
// httpServer
// handler
}
func New() Server {
return Server{}
}
func (s Server) Serve() {}
func (s Server) RegisterRoutes() {}

View File

@ -1 +0,0 @@
package repository

View File

@ -1,5 +0,0 @@
package service
type Agent struct {
ID uint
}

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

View File

@ -0,0 +1,23 @@
package command
import "github.com/spf13/cobra"
var up bool
var down bool
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Run database migrations",
Long: `This command runs the database migrations for the account service.`,
Run: func(cmd *cobra.Command, args []string) {
migrate()
},
}
func migrate() {}
func init() {
migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up")
migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down")
RootCmd.AddCommand(migrateCmd)
}

View File

@ -0,0 +1,10 @@
package command
import "github.com/spf13/cobra"
var RootCmd = &cobra.Command{
Use: "account_service",
Short: "A CLI for account Service",
Long: `account Service CLI is a tool to manage and run
the account service, including migrations and server startup.`,
}

View File

@ -0,0 +1,14 @@
package command
import "github.com/spf13/cobra"
var serveCmd = &cobra.Command{
Use: "serve",
Short: "start a account service.",
Long: `This command starts the main account service.`,
Run: func(cmd *cobra.Command, args []string) {
serve()
},
}
func serve() {}

14
cmd/account/main.go Normal file
View File

@ -0,0 +1,14 @@
package account
import (
"os"
"git.gocasts.ir/ebhomengo/niki/cmd/account/command"
)
func main() {
if err := command.RootCmd.Execute(); err != nil {
os.Exit(1)
}
}

52
cmd/driverapp/main.go Normal file
View File

@ -0,0 +1,52 @@
package driverapp
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"git.gocasts.ir/ebhomengo/niki/driverapp"
cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader"
"git.gocasts.ir/ebhomengo/niki/pkg/migrator"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
func main() {
var cfg driverapp.Config
workingDir, err := os.Getwd()
if err != nil {
fmt.Printf("Error getting current working directory: %v", err)
}
options := cfgloader.Option{
Prefix: "DRIVER_",
Delimiter: ".",
Separator: "__",
YamlFilePath: filepath.Join(workingDir, "deploy", "driver", "development", "config.yaml"),
CallbackEnv: nil,
}
lErr := cfgloader.Load(options, &cfg)
if lErr != nil {
log.Fatalf("Failed to load driver config: %v", err)
}
conn := mysql.New(cfg.MysqlDB)
mgr := migrator.New(cfg.MysqlDB, cfg.PathOfMigration)
migrate := flag.Bool("migrate", false, "perform database migrations")
flag.Parse()
if *migrate {
fmt.Println("Running migrations")
mgr.Up()
}
//dapp := driverapp.Setup(cfg)
//dapp.Start()
}

View File

@ -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() {

View File

@ -0,0 +1,281 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v3.21.12
// source: contract/protobuf/account/account.proto
package account
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type LoginOrRegisterRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
PhoneNumber string `protobuf:"bytes,1,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"`
VerifyCode string `protobuf:"bytes,2,opt,name=verifyCode,proto3" json:"verifyCode,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginOrRegisterRequest) Reset() {
*x = LoginOrRegisterRequest{}
mi := &file_contract_protobuf_account_account_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginOrRegisterRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginOrRegisterRequest) ProtoMessage() {}
func (x *LoginOrRegisterRequest) ProtoReflect() protoreflect.Message {
mi := &file_contract_protobuf_account_account_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginOrRegisterRequest.ProtoReflect.Descriptor instead.
func (*LoginOrRegisterRequest) Descriptor() ([]byte, []int) {
return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{0}
}
func (x *LoginOrRegisterRequest) GetPhoneNumber() string {
if x != nil {
return x.PhoneNumber
}
return ""
}
func (x *LoginOrRegisterRequest) GetVerifyCode() string {
if x != nil {
return x.VerifyCode
}
return ""
}
type LoginOrRegisterResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
PhoneNumber string `protobuf:"bytes,2,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginOrRegisterResponse) Reset() {
*x = LoginOrRegisterResponse{}
mi := &file_contract_protobuf_account_account_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginOrRegisterResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginOrRegisterResponse) ProtoMessage() {}
func (x *LoginOrRegisterResponse) ProtoReflect() protoreflect.Message {
mi := &file_contract_protobuf_account_account_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginOrRegisterResponse.ProtoReflect.Descriptor instead.
func (*LoginOrRegisterResponse) Descriptor() ([]byte, []int) {
return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{1}
}
func (x *LoginOrRegisterResponse) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *LoginOrRegisterResponse) GetPhoneNumber() string {
if x != nil {
return x.PhoneNumber
}
return ""
}
type SendOtpRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
PhoneNumber string `protobuf:"bytes,1,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SendOtpRequest) Reset() {
*x = SendOtpRequest{}
mi := &file_contract_protobuf_account_account_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SendOtpRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SendOtpRequest) ProtoMessage() {}
func (x *SendOtpRequest) ProtoReflect() protoreflect.Message {
mi := &file_contract_protobuf_account_account_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SendOtpRequest.ProtoReflect.Descriptor instead.
func (*SendOtpRequest) Descriptor() ([]byte, []int) {
return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{2}
}
func (x *SendOtpRequest) GetPhoneNumber() string {
if x != nil {
return x.PhoneNumber
}
return ""
}
type SendOtpResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SendOtpResponse) Reset() {
*x = SendOtpResponse{}
mi := &file_contract_protobuf_account_account_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SendOtpResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SendOtpResponse) ProtoMessage() {}
func (x *SendOtpResponse) ProtoReflect() protoreflect.Message {
mi := &file_contract_protobuf_account_account_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SendOtpResponse.ProtoReflect.Descriptor instead.
func (*SendOtpResponse) Descriptor() ([]byte, []int) {
return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{3}
}
var File_contract_protobuf_account_account_proto protoreflect.FileDescriptor
const file_contract_protobuf_account_account_proto_rawDesc = "" +
"\n" +
"'contract/protobuf/account/account.proto\x12\asendOtp\"Z\n" +
"\x16LoginOrRegisterRequest\x12 \n" +
"\vphoneNumber\x18\x01 \x01(\tR\vphoneNumber\x12\x1e\n" +
"\n" +
"verifyCode\x18\x02 \x01(\tR\n" +
"verifyCode\"K\n" +
"\x17LoginOrRegisterResponse\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12 \n" +
"\vphoneNumber\x18\x02 \x01(\tR\vphoneNumber\"2\n" +
"\x0eSendOtpRequest\x12 \n" +
"\vphoneNumber\x18\x01 \x01(\tR\vphoneNumber\"\x11\n" +
"\x0fSendOtpResponse2\xa4\x01\n" +
"\x0eAccountService\x12<\n" +
"\aSendOtp\x12\x17.sendOtp.SendOtpRequest\x1a\x18.sendOtp.SendOtpResponse\x12T\n" +
"\x0fLoginOrRegister\x12\x1f.sendOtp.LoginOrRegisterRequest\x1a .sendOtp.LoginOrRegisterResponseB\x1dZ\x1bcontract/goprotobuf/accountb\x06proto3"
var (
file_contract_protobuf_account_account_proto_rawDescOnce sync.Once
file_contract_protobuf_account_account_proto_rawDescData []byte
)
func file_contract_protobuf_account_account_proto_rawDescGZIP() []byte {
file_contract_protobuf_account_account_proto_rawDescOnce.Do(func() {
file_contract_protobuf_account_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_contract_protobuf_account_account_proto_rawDesc), len(file_contract_protobuf_account_account_proto_rawDesc)))
})
return file_contract_protobuf_account_account_proto_rawDescData
}
var file_contract_protobuf_account_account_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_contract_protobuf_account_account_proto_goTypes = []any{
(*LoginOrRegisterRequest)(nil), // 0: sendOtp.LoginOrRegisterRequest
(*LoginOrRegisterResponse)(nil), // 1: sendOtp.LoginOrRegisterResponse
(*SendOtpRequest)(nil), // 2: sendOtp.SendOtpRequest
(*SendOtpResponse)(nil), // 3: sendOtp.SendOtpResponse
}
var file_contract_protobuf_account_account_proto_depIdxs = []int32{
2, // 0: sendOtp.AccountService.SendOtp:input_type -> sendOtp.SendOtpRequest
0, // 1: sendOtp.AccountService.LoginOrRegister:input_type -> sendOtp.LoginOrRegisterRequest
3, // 2: sendOtp.AccountService.SendOtp:output_type -> sendOtp.SendOtpResponse
1, // 3: sendOtp.AccountService.LoginOrRegister:output_type -> sendOtp.LoginOrRegisterResponse
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_contract_protobuf_account_account_proto_init() }
func file_contract_protobuf_account_account_proto_init() {
if File_contract_protobuf_account_account_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_contract_protobuf_account_account_proto_rawDesc), len(file_contract_protobuf_account_account_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_contract_protobuf_account_account_proto_goTypes,
DependencyIndexes: file_contract_protobuf_account_account_proto_depIdxs,
MessageInfos: file_contract_protobuf_account_account_proto_msgTypes,
}.Build()
File_contract_protobuf_account_account_proto = out.File
file_contract_protobuf_account_account_proto_goTypes = nil
file_contract_protobuf_account_account_proto_depIdxs = nil
}

View File

@ -0,0 +1,159 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v3.21.12
// source: contract/protobuf/account/account.proto
package account
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
AccountService_SendOtp_FullMethodName = "/sendOtp.AccountService/SendOtp"
AccountService_LoginOrRegister_FullMethodName = "/sendOtp.AccountService/LoginOrRegister"
)
// AccountServiceClient is the client API for AccountService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AccountServiceClient interface {
SendOtp(ctx context.Context, in *SendOtpRequest, opts ...grpc.CallOption) (*SendOtpResponse, error)
LoginOrRegister(ctx context.Context, in *LoginOrRegisterRequest, opts ...grpc.CallOption) (*LoginOrRegisterResponse, error)
}
type accountServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAccountServiceClient(cc grpc.ClientConnInterface) AccountServiceClient {
return &accountServiceClient{cc}
}
func (c *accountServiceClient) SendOtp(ctx context.Context, in *SendOtpRequest, opts ...grpc.CallOption) (*SendOtpResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SendOtpResponse)
err := c.cc.Invoke(ctx, AccountService_SendOtp_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accountServiceClient) LoginOrRegister(ctx context.Context, in *LoginOrRegisterRequest, opts ...grpc.CallOption) (*LoginOrRegisterResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LoginOrRegisterResponse)
err := c.cc.Invoke(ctx, AccountService_LoginOrRegister_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AccountServiceServer is the server API for AccountService service.
// All implementations must embed UnimplementedAccountServiceServer
// for forward compatibility.
type AccountServiceServer interface {
SendOtp(context.Context, *SendOtpRequest) (*SendOtpResponse, error)
LoginOrRegister(context.Context, *LoginOrRegisterRequest) (*LoginOrRegisterResponse, error)
mustEmbedUnimplementedAccountServiceServer()
}
// UnimplementedAccountServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAccountServiceServer struct{}
func (UnimplementedAccountServiceServer) SendOtp(context.Context, *SendOtpRequest) (*SendOtpResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SendOtp not implemented")
}
func (UnimplementedAccountServiceServer) LoginOrRegister(context.Context, *LoginOrRegisterRequest) (*LoginOrRegisterResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LoginOrRegister not implemented")
}
func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {}
func (UnimplementedAccountServiceServer) testEmbeddedByValue() {}
// UnsafeAccountServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AccountServiceServer will
// result in compilation errors.
type UnsafeAccountServiceServer interface {
mustEmbedUnimplementedAccountServiceServer()
}
func RegisterAccountServiceServer(s grpc.ServiceRegistrar, srv AccountServiceServer) {
// If the following call panics, it indicates UnimplementedAccountServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AccountService_ServiceDesc, srv)
}
func _AccountService_SendOtp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SendOtpRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountServiceServer).SendOtp(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccountService_SendOtp_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountServiceServer).SendOtp(ctx, req.(*SendOtpRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AccountService_LoginOrRegister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginOrRegisterRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountServiceServer).LoginOrRegister(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccountService_LoginOrRegister_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountServiceServer).LoginOrRegister(ctx, req.(*LoginOrRegisterRequest))
}
return interceptor(ctx, in, info, handler)
}
// AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AccountService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "sendOtp.AccountService",
HandlerType: (*AccountServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SendOtp",
Handler: _AccountService_SendOtp_Handler,
},
{
MethodName: "LoginOrRegister",
Handler: _AccountService_LoginOrRegister_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "contract/protobuf/account/account.proto",
}

View File

@ -0,0 +1,28 @@
syntax = "proto3";
package sendOtp;
option go_package = "contract/goprotobuf/account";
message LoginOrRegisterRequest{
string phoneNumber = 1;
string verifyCode = 2;
}
message LoginOrRegisterResponse {
uint64 id = 1;
string phoneNumber = 2;
}
message SendOtpRequest {
string phoneNumber = 1;
}
message SendOtpResponse {}
service AccountService {
rpc SendOtp(SendOtpRequest) returns (SendOtpResponse);
rpc LoginOrRegister(LoginOrRegisterRequest) returns(LoginOrRegisterResponse);
}

View File

@ -8,7 +8,7 @@ import (
func MigrateMariaDB(cfg mysql.Config) func() {
migrations := migrator.New(migrator.Config{
MysqlConfig: cfg,
MigrationPath: "../../../repository/mysql/migration",
MigrationPath: "../../../repository/mysql/migrations",
MigrationDBName: "gorp_migrations",
})
migrations.Up()

View File

@ -0,0 +1,27 @@
service:
length_of_otp_code: 6
otp_chars: "0123456789"
otp_expire_time: 2
redis_db:
host:
port:
password:
db:
mysql_db:
username:
password:
port:
host:
db_name:
kavenegar:
api_key:
sender:
grpc_server:
port:
network:
grpc_client:
host:
port:
path_of_migration: ./account/repository/mysql/migration

View File

View File

@ -0,0 +1,20 @@
service:
length_of_otp_code: 6
otp_chars: "0123456789"
otp_expire_time: 2
redis_db:
host:
port:
password:
db:
mysql_db:
username:
password:
port:
host:
db_name:
kavenegar:
api_key:
sender:
path_of_migration: ./driverapp/repository/mysql/migration

View File

@ -0,0 +1,29 @@
services:
driver_mariadb:
image: bitnami/mariadb:11.1
container_name: driver_mariadb
restart: always
ports:
- "3305:3306"
volumes:
- 'driver-mariadb-data:/bitnami/mariadb'
environment:
MARIADB_USER: driver_admin
MARIADB_PASSWORD: password123
MARIADB_DATABASE: driver_db
MARIADB_ROOT_PASSWORD: password123
driver_redis:
image: bitnami/redis:6.2
container_name: driver-redis
restart: always
ports:
- '6380:6379'
command: redis-server --loglevel warning --protected-mode no --save "" --appendonly no
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:
- driver-redis-data:/data
volumes:
driver-mariadb-data:
driver-redis-data:

View File

@ -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"]

View File

@ -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:

View File

@ -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:

View File

View File

View File

View File

@ -0,0 +1,8 @@
package entity
import "git.gocasts.ir/ebhomengo/niki/pkg/types"
type Driver struct {
ID types.ID
PhoneNumber string
}

View File

@ -0,0 +1,87 @@
package mysql
import (
"context"
"database/sql"
"errors"
"time"
"git.gocasts.ir/ebhomengo/niki/domain/account/entity"
"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
types "git.gocasts.ir/ebhomengo/niki/pkg/types"
)
const (
StatementKeyIsExistDriverByPhoneNumber = iota + 1
StatementKeyCreateDriver = iota + 1
)
type AccountRepo struct {
db *mysql.DB
}
func New(db *mysql.DB) AccountRepo {
return AccountRepo{
db: db,
}
}
func (r AccountRepo) IsExistDriverByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Driver, error) {
const op = "Repository.IsExistDriverByPhoneNumber"
query := `select * from drivers where phone_number = ?`
stmt, err := r.db.PrepareStatement(ctx, StatementKeyIsExistDriverByPhoneNumber, query)
if err != nil {
return false, entity.Driver{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected).
WithMessage(errmsg.ErrorMsgCantPrepareStatement)
}
defer stmt.Close()
row := stmt.QueryRowContext(ctx, phoneNumber)
d, sErr := DriverScan(row)
if sErr != nil {
if errors.Is(sErr, sql.ErrNoRows) {
return false, entity.Driver{}, richerror.New(op).WithKind(richerror.KindNotFound).
WithMessage(errmsg.ErrorMsgNotFound)
}
return false, entity.Driver{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
}
return true, d, nil
}
func (r AccountRepo) CreateDriver(ctx context.Context, driver entity.Driver) (entity.Driver, error) {
const op = "Repository.CreateDriver"
query := `insert into drivers(phone_number) values(?)`
stmt, err := r.db.PrepareStatement(ctx, StatementKeyCreateDriver, query)
if err != nil {
return entity.Driver{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected).
WithMessage(errmsg.ErrorMsgCantPrepareStatement)
}
res, err := stmt.ExecContext(ctx, driver.PhoneNumber)
if err != nil {
return entity.Driver{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected).
WithMessage(errmsg.ErrorMsgNotFound)
}
id, _ := res.LastInsertId()
driver.ID = types.ID(id)
return driver, nil
}
func DriverScan(scanner mysql.Scanner) (entity.Driver, error) {
var createdAt, updatedAt time.Time
var driver entity.Driver
err := scanner.Scan(&driver.ID, &driver.PhoneNumber, &createdAt, &updatedAt)
return driver, err
}

View File

@ -0,0 +1,13 @@
-- +migrate Up
CREATE TABLE `drivers`(
`iD` INT PRIMARY KEY AUTO_INCREMENT,
`phone_number` VARCHAR(191) NOT NULL UNIQUE ,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +migrate Down
DROP TABLE `drivers`;

View File

@ -0,0 +1,67 @@
package redis
import (
"context"
"time"
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
type RepositoryOtp struct {
conn *redis.Adapter
}
func NewRepositoryOtp(conn *redis.Adapter) RepositoryOtp {
return RepositoryOtp{conn: conn}
}
func (r RepositoryOtp) IsExistPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) {
const op = "RepositoryOtp.IsExistPhoneNumber"
result, err := r.conn.Client().Exists(ctx, phoneNumber).Result()
if err != nil {
return false, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if result == 0 {
return false, nil
}
return true, nil
}
func (r RepositoryOtp) SaveCodeWithPhoneNumber(ctx context.Context, phoneNumber string, code string, expireTime time.Duration) error {
const op = "RepositoryOtp.SaveCodeWithPhoneNumber"
_, err := r.conn.Client().Set(ctx, phoneNumber, code, expireTime).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}
func (r RepositoryOtp) GetCodeByPhoneNumber(ctx context.Context, phoneNumber string) (string, error) {
const op = "RepositoryOtp.GetCodeByPhoneNumber"
result, err := r.conn.Client().Get(ctx, phoneNumber).Result()
if err != nil {
return "", richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return result, nil
}
func (r RepositoryOtp) DeleteCodeByPhoneNumber(ctx context.Context, PhoneNumber string) (bool, error) {
const op = "RepositoryOtp.DeleteCodeByPhoneNumber"
success, err := r.conn.Client().Del(ctx, PhoneNumber).Result()
if err != nil {
return false, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected)
}
if success != 1 {
return false, nil
}
return true, nil
}

View File

@ -0,0 +1,115 @@
package service
import (
"context"
"math/rand"
"time"
smscontract "git.gocasts.ir/ebhomengo/niki/contract/sms"
"git.gocasts.ir/ebhomengo/niki/domain/account/entity"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
type Config struct {
LengthOfOtpCode int `koanf:"length_of_otp_code"`
OtpChars string `koanf:"otp_chars"`
OtpExpireTime time.Duration `koanf:"otp_expire_time"`
}
type RepositoryOtp interface {
IsExistPhoneNumber(ctx context.Context, phoneNumber string) (bool, error)
SaveCodeWithPhoneNumber(ctx context.Context, phoneNumber string, code string, expireTime time.Duration) error
GetCodeByPhoneNumber(ctx context.Context, phoneNumber string) (string, error)
DeleteCodeByPhoneNumber(ctx context.Context, PhoneNumber string) (bool, error)
}
type Repository interface {
IsExistDriverByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Driver, error)
CreateDriver(ctx context.Context, driver entity.Driver) (entity.Driver, error)
}
type Service struct {
config Config
repositoryOtp RepositoryOtp
repository Repository
smsContract smscontract.SmsAdapter
}
func NewService(cfg Config, repositoryOtp RepositoryOtp, repository Repository, smsContract smscontract.SmsAdapter) Service {
return Service{
config: cfg,
repositoryOtp: repositoryOtp,
repository: repository,
smsContract: smsContract,
}
}
func (s Service) SendOTP(ctx context.Context, phoneNumber string) error {
const op = "accountService.SendOTP"
isExist, iErr := s.repositoryOtp.IsExistPhoneNumber(ctx, phoneNumber)
if iErr != nil {
return richerror.New(op).WithErr(iErr).WithKind(richerror.KindUnexpected)
}
if isExist {
return richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeExist).WithKind(richerror.KindForbidden)
}
newCode := s.generateVerificationCode()
sErr := s.repositoryOtp.SaveCodeWithPhoneNumber(ctx, phoneNumber, newCode, s.config.OtpExpireTime)
if sErr != nil {
return richerror.New(op).WithErr(sErr).WithKind(richerror.KindUnexpected)
}
go s.smsContract.Send(phoneNumber, newCode)
return nil
}
func (s Service) LoginOrRegisterDriver(ctx context.Context, phoneNumber string, verifyCode string) (entity.Driver, error) {
const op = "accountService.LoginOrRegisterDriver"
code, gErr := s.repositoryOtp.GetCodeByPhoneNumber(ctx, phoneNumber)
if gErr != nil {
return entity.Driver{}, richerror.New(op).WithErr(gErr).WithKind(richerror.KindUnexpected)
}
if code == "" || code != verifyCode {
return entity.Driver{}, richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeIsNotValid).WithKind(richerror.KindForbidden)
}
_, dErr := s.repositoryOtp.DeleteCodeByPhoneNumber(ctx, phoneNumber)
if dErr != nil {
return entity.Driver{}, richerror.New(op).WithErr(dErr).WithKind(richerror.KindUnexpected)
}
isExist, driver, eErr := s.repository.IsExistDriverByPhoneNumber(ctx, phoneNumber)
if eErr != nil {
return entity.Driver{}, richerror.New(op).WithErr(eErr).WithKind(richerror.KindUnexpected)
}
if !isExist {
newDriver, cErr := s.repository.CreateDriver(ctx, entity.Driver{
PhoneNumber: phoneNumber,
})
if cErr != nil {
return entity.Driver{}, richerror.New(op).WithErr(cErr).WithKind(richerror.KindUnexpected)
}
driver = newDriver
}
return driver, nil
}
func (s Service) generateVerificationCode() string {
result := make([]byte, s.config.LengthOfOtpCode)
for i := 0; i < s.config.LengthOfOtpCode; i++ {
result[i] = s.config.OtpChars[rand.Intn(len(s.config.OtpChars))]
}
return string(result)
}

View File

@ -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}
}

View File

@ -0,0 +1 @@
package mysql

View File

@ -1,8 +1,8 @@
package service
type Service struct {
repo Repo
}
func New() Service {
return Service{}
type Repo interface {
}

View File

@ -0,0 +1,16 @@
package entity
import (
"encoding/json"
"time"
)
type Gateway struct {
ID uint
Name string
Code string
IsActive bool
Config json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -0,0 +1,45 @@
package entity
import "time"
type Currency string
const (
CurrencyIRR Currency = "IRR"
CurrencyUSD Currency = "USD"
)
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "Pending"
PaymentStatusSuccess PaymentStatus = "Success"
PaymentStatusFailed PaymentStatus = "Failed"
PaymentStatusCancelled PaymentStatus = "Cancelled"
//...
)
type PayableType string
const (
PayableTypeDonate PayableType = "Donate"
PayableTypeOrder PayableType = "Order"
PayableTypeWalet PayableType = "WaletCharge"
)
type Payment struct {
ID uint
UserID uint
MethodID uint
GatewayID uint
PayableType PayableType
PayableID uint
TotalAmount int64
PaidAmount int64
Currency Currency
Status PaymentStatus
Description string
CreatedAt time.Time
UpdatedAt time.Time
PaidAt *time.Time
}

View File

@ -0,0 +1,11 @@
package entity
import "time"
type PaymentMethod struct {
ID uint
Name string
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -0,0 +1,35 @@
package entity
import (
"encoding/json"
"time"
)
type TransactionType string
const (
TransactionTypeRequest TransactionType = "request"
TransactionTypeVerify TransactionType = "verify"
)
type TransactionStatus string
const (
TransactionStatusPending = "Pending"
TransactionStatusSuccess = "Success"
TransactionStatusFailed = "Failed"
)
type PaymentTransaction struct {
ID uint
PaymentID uint
Type TransactionType
RequestData json.RawMessage
ResponseData json.RawMessage
RefID string
Status TransactionStatus
GatewayToken string
ErrorMessage string
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -12,7 +12,7 @@ func NewGatewayFactory() service.GatewayFactory {
return &gatewayFactoryImpl{}
}
func (f *gatewayFactoryImpl) GetGatewayAdapter(code string) (service.GatewayPort, error) {
func (f gatewayFactoryImpl) GetGatewayAdapter(code string) (service.GatewayPort, error) {
switch code {
case "melat":
return nil, nil

View File

@ -1,13 +1,31 @@
package repository
import (
"context"
"database/sql"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentRepository struct {
DB *sql.DB
}
// CreatePayment implements [service.PaymentRepo].
func (*PaymentRepository) CreatePayment(ctx context.Context, p *entity.Payment) error {
panic("unimplemented")
}
// CreateTransaction implements [service.PaymentRepo].
func (p *PaymentRepository) CreateTransaction(ctx context.Context, t *entity.PaymentTransaction) error {
panic("unimplemented")
}
// UpdateTransaction implements [service.PaymentRepo].
func (p *PaymentRepository) UpdateTransaction(ctx context.Context, t *entity.PaymentTransaction) error {
panic("unimplemented")
}
func NewPaymentRepository(db *sql.DB) *PaymentRepository {
return &PaymentRepository{
DB: db,

View File

@ -0,0 +1,23 @@
package repository
import (
"context"
"database/sql"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentMethodRepository struct {
DB *sql.DB
}
// GetGatewayByCode implements [service.PaymentMethodRepo].
func (p *PaymentMethodRepository) GetGatewayByCode(ctx context.Context, code string) (entity.Gateway, error) {
panic("unimplemented")
}
func NewPaymentMethodRepository(db *sql.DB) *PaymentMethodRepository {
return &PaymentMethodRepository{
DB: db,
}
}

View File

@ -1,94 +0,0 @@
package service
import (
"encoding/json"
"time"
)
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "Pending"
PaymentStatusSuccess PaymentStatus = "Success"
PaymentStatusFailed PaymentStatus = "Failed"
PaymentStatusCancelled PaymentStatus = "Cancelled"
//...
)
type Currency string
const (
CurrencyIRR Currency = "IRR"
CurrencyUSD Currency = "USD"
)
type TransactionType string
const (
TransactionTypeRequest TransactionType = "request"
TransactionTypeVerify TransactionType = "verify"
)
type TransactionStatus string
const (
TransactionStatusPending = "Pending"
TransactionStatusSuccess = "Success"
TransactionStatusFailed = "Failed"
)
type PayableType string
const (
PayableTypeDonate PayableType = "Donate"
PayableTypeOrder PayableType = "Order"
PayableTypeWalet PayableType = "WaletCharge"
)
type PaymentMethod struct {
ID uint
Name string
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}
type Gateway struct {
ID uint
Name string
Code string
IsActive bool
Config json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}
type Payment struct {
ID uint
UserID uint
MethodID uint
GatewayID uint
PayableType PayableType
PayableID uint
TotalAmount int64
PaidAmount int64
Currency Currency
Status PaymentStatus
Description string
CreatedAt time.Time
UpdatedAt time.Time
PaidAt *time.Time
}
type PaymentTransaction struct {
ID uint
PaymentID uint
Type TransactionType
RequestData json.RawMessage
ResponseData json.RawMessage
RefID string
Status TransactionStatus
GatewayToken string
ErrorMessage string
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -1,8 +1,10 @@
package service
import "git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
type InitiatePaymentRequest struct {
UserID uint
PayableType PayableType
PayableType entity.PayableType
PayableID uint
GatewayCode string
CallbackURL string

View File

@ -5,18 +5,23 @@ import (
"errors"
"fmt"
"time"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentService struct {
repo PaymentRepository
paymentRepo PaymentRepo
paymentMethodRepo PaymentMethodRepo
gwFactory GatewayFactory
}
type PaymentRepository interface {
CreatePayment(ctx context.Context, p *Payment) error
CreateTransaction(ctx context.Context, t *PaymentTransaction) error
UpdateTransaction(ctx context.Context, t *PaymentTransaction) error
GetGatewayByCode(ctx context.Context, code string) (*Gateway, error)
type PaymentRepo interface {
CreatePayment(ctx context.Context, p *entity.Payment) error
CreateTransaction(ctx context.Context, t *entity.PaymentTransaction) error
UpdateTransaction(ctx context.Context, t *entity.PaymentTransaction) error
}
type PaymentMethodRepo interface {
GetGatewayByCode(ctx context.Context, code string) (entity.Gateway, error)
}
type GatewayFactory interface {
@ -27,43 +32,44 @@ type GatewayPort interface {
Request(amount int64, callbackURL string, description string) (token string, redirectURL string, rawReq []byte, rawRes []byte, err error)
}
func NewPaymentService(r PaymentRepository, gwf GatewayFactory) *PaymentService {
func NewPaymentService(pr PaymentRepo, pmr PaymentMethodRepo, gwf GatewayFactory) *PaymentService {
return &PaymentService{
repo: r,
paymentRepo: pr,
paymentMethodRepo: pmr,
gwFactory: gwf,
}
}
func (s *PaymentService) InitiatePayment(ctx context.Context, req InitiatePaymentRequest) (*InitiatePaymentResponse, error) {
gateway, err := s.repo.GetGatewayByCode(ctx, req.GatewayCode)
gateway, err := s.paymentMethodRepo.GetGatewayByCode(ctx, req.GatewayCode)
if err != nil || !gateway.IsActive {
return nil, errors.New("gateway is not available")
}
payment := &Payment{
payment := &entity.Payment{
UserID: req.UserID,
PayableType: req.PayableType,
PayableID: req.PayableID,
GatewayID: gateway.ID,
TotalAmount: req.Amount,
Currency: CurrencyIRR,
Status: PaymentStatusPending,
Currency: entity.CurrencyIRR,
Status: entity.PaymentStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repo.CreatePayment(ctx, payment); err != nil {
if err := s.paymentRepo.CreatePayment(ctx, payment); err != nil {
return nil, fmt.Errorf("failed to create payment: %w", err)
}
transaction := &PaymentTransaction{
transaction := &entity.PaymentTransaction{
PaymentID: payment.ID,
Type: TransactionTypeRequest,
Status: TransactionStatusPending,
Type: entity.TransactionTypeRequest,
Status: entity.TransactionStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repo.CreateTransaction(ctx, transaction); err != nil {
if err := s.paymentRepo.CreateTransaction(ctx, transaction); err != nil {
return nil, fmt.Errorf("failed to create initial transaction: %w", err)
}
@ -80,15 +86,15 @@ func (s *PaymentService) InitiatePayment(ctx context.Context, req InitiatePaymen
transaction.UpdatedAt = time.Now()
if gwErr != nil {
transaction.Status = TransactionStatusFailed
transaction.Status = entity.TransactionStatusFailed
transaction.ErrorMessage = gwErr.Error()
_ = s.repo.UpdateTransaction(ctx, transaction)
_ = s.paymentRepo.UpdateTransaction(ctx, transaction)
return nil, fmt.Errorf("gateway request failed: %w", gwErr)
}
transaction.Status = TransactionStatusSuccess
transaction.Status = entity.TransactionStatusSuccess
transaction.GatewayToken = token
if err := s.repo.UpdateTransaction(ctx, transaction); err != nil {
if err := s.paymentRepo.UpdateTransaction(ctx, transaction); err != nil {
return nil, fmt.Errorf("failed to update transaction with token: %w", err)
}

View File

@ -0,0 +1,4 @@
package service
type PaymentMethodService struct {
}

View File

@ -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"
)

View File

@ -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)
)

View File

@ -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 {
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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}
}

View File

@ -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

View File

@ -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
}

View File

@ -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}
}

View File

@ -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
}

View File

@ -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
}

38
driverapp/app.go Normal file
View File

@ -0,0 +1,38 @@
package driverapp
import (
"git.gocasts.ir/ebhomengo/niki/adapter/account"
"git.gocasts.ir/ebhomengo/niki/driverapp/delivery/http"
"git.gocasts.ir/ebhomengo/niki/driverapp/service"
"git.gocasts.ir/ebhomengo/niki/pkg/http_server"
"google.golang.org/grpc"
)
type Application struct {
svc service.Service
accountClient account.Client
handler http.Handler
httpServer http.Server
config Config
}
func Setup(config Config, conn *grpc.ClientConn) Application {
driverValidator := service.NewValidator()
accountClient := account.New(conn)
driverSvc := service.NewService(config.DriverSvc, accountClient, driverValidator)
driverHandler := http.NewHandler(driverSvc)
httpServer := httpserver.New(config.HttpServer)
return Application{
svc: driverSvc,
handler: driverHandler,
httpServer: http.New(httpServer, driverHandler),
config: config,
}
}
func (app Application) Start() {
app.httpServer.Serve()
}

18
driverapp/config.go Normal file
View File

@ -0,0 +1,18 @@
package driverapp
import (
"git.gocasts.ir/ebhomengo/niki/adapter/kavenegar"
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/driverapp/service"
"git.gocasts.ir/ebhomengo/niki/pkg/http_server"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
type Config struct {
DriverSvc service.Config `koanf:"service"`
HttpServer http_server.Config `koanf:"http_server"`
Redis redis.Config `koanf:"redis_db"`
MysqlDB mysql.Config `koanf:"mysql_db"`
Kavenegar kavenegar.Config `koanf:"kavenegar"`
PathOfMigration string `koanf:"path_of_migration"`
}

View File

@ -0,0 +1,51 @@
package http
import (
"net/http"
"git.gocasts.ir/ebhomengo/niki/driverapp/service"
httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg"
"github.com/labstack/echo/v4"
)
type Handler struct {
DriverSvc service.Service
}
func NewHandler(driverSvc service.Service) Handler {
return Handler{
DriverSvc: driverSvc,
}
}
func (h Handler) SendOtp(c echo.Context) error {
var req service.SendOtpRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
res, err := h.DriverSvc.SendOtp(c.Request().Context(), req)
if err != nil {
msg, code := httpmsg.Error(err)
return echo.NewHTTPError(code, msg)
}
return c.JSON(http.StatusOK, res)
}
func (h Handler) loginOrRegister(c echo.Context) error {
var req service.LoginOrRegisterRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
res, err := h.DriverSvc.LoginOrRegister(c.Request().Context(), req)
if err != nil {
msg, code := httpmsg.Error(err)
return echo.NewHTTPError(code, msg)
}
return c.JSON(http.StatusOK, res)
}

View File

@ -0,0 +1,13 @@
package http
import (
"net/http"
"github.com/labstack/echo/v4"
)
func (s Server) HealthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, echo.Map{
"message": "everything is good!",
})
}

View File

@ -0,0 +1,36 @@
package http
import (
"fmt"
"git.gocasts.ir/ebhomengo/niki/pkg/http_server"
)
type Server struct {
HTTPServer http_server.Server
Handler Handler
}
func New(server http_server.Server, handler Handler) Server {
return Server{
HTTPServer: server,
Handler: handler,
}
}
func (s Server) Serve() {
s.RegisterRoutes()
if err := s.HTTPServer.Start(); err != nil {
fmt.Println("router start error", err)
}
}
func (s Server) RegisterRoutes() {
v1 := s.HTTPServer.Router.Group("/v1")
v1.GET("/health_check", s.HealthCheck)
v1.POST("/send_otp", s.Handler.SendOtp)
v1.POST("/login_or_register", s.Handler.loginOrRegister)
}

View File

@ -0,0 +1,17 @@
package entity
import (
"time"
"git.gocasts.ir/ebhomengo/niki/pkg/types"
)
type Driver struct {
ID types.ID
FirstName string
LastName string
PhoneNumber string
NationalCode string
LicenseNumber string
BirthDate time.Time
}

View File

@ -0,0 +1,25 @@
package service
import "git.gocasts.ir/ebhomengo/niki/pkg/types"
type LoginOrRegisterRequest struct {
PhoneNumber string `json:"phone_number"`
VerifyCode string `json:"verify_code"`
}
type LoginOrRegisterResponse struct {
ID types.ID `json:"id"`
PhoneNumber string `json:"phone_number"`
}
type Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type SendOtpRequest struct {
PhoneNumber string `json:"phone_number"`
}
type SendOtpResponse struct {
}

View File

@ -0,0 +1,76 @@
package service
import (
"context"
"fmt"
"time"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
type Config struct {
LengthOfOtpCode int `koanf:"length_of_otp_code"`
OtpChars string `koanf:"otp_chars"`
OtpExpireTime time.Duration `koanf:"otp_expire_time"`
}
type AccountClient interface {
SendOTP(ctx context.Context, phoneNumber string) error
LoginOrRegister(ctx context.Context, req LoginOrRegisterRequest) (LoginOrRegisterResponse, error)
}
type AuthClient interface {
CreateAccessToken()
CreateRefreshToken()
}
type Service struct {
config Config
accountClient AccountClient
validator Validator
}
func NewService(cfg Config,
accountClient AccountClient,
validator Validator) Service {
return Service{
config: cfg,
accountClient: accountClient,
validator: validator,
}
}
func (s Service) SendOtp(ctx context.Context, req SendOtpRequest) (SendOtpResponse, error) {
const op = "driverService.SendOtp"
err := s.validator.ValidateSendOtpRequest(req)
if err != nil {
return SendOtpResponse{}, richerror.New(op).WithErr(err).WithMessage(err.Error())
}
sErr := s.accountClient.SendOTP(ctx, req.PhoneNumber)
if sErr != nil {
return SendOtpResponse{}, richerror.New(op).WithErr(sErr).WithMessage(sErr.Error())
}
return SendOtpResponse{}, nil
}
func (s Service) LoginOrRegister(ctx context.Context, req LoginOrRegisterRequest) (LoginOrRegisterResponse, error) {
const op = "driverService.LoginOrRegister"
err := s.validator.ValidateLoginOrRegisterRequest(req)
if err != nil {
return LoginOrRegisterResponse{}, richerror.New(op).WithErr(err).WithMessage(err.Error())
}
resp, lErr := s.accountClient.LoginOrRegister(ctx, req)
if lErr != nil {
return LoginOrRegisterResponse{}, err
}
fmt.Println("res:", resp)
// TODO : CreateAccessToken and create CreateRefreshToken
return LoginOrRegisterResponse{}, nil
}

View File

@ -0,0 +1,40 @@
package service
import (
"regexp"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
validation "github.com/go-ozzo/ozzo-validation"
)
const (
PhoneNumberRegex = "^(0|0098|\\+98)9(0[1-5]|[1 3]\\d|2[0-2]|98)\\d{7}$"
)
type Validator struct{}
func NewValidator() Validator {
return Validator{}
}
func (v Validator) ValidateSendOtpRequest(req SendOtpRequest) error {
err := validation.ValidateStruct(&req,
validation.Field(req.PhoneNumber,
validation.Required,
validation.Match(regexp.MustCompile(PhoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid),
))
return err
}
func (v Validator) ValidateLoginOrRegisterRequest(req LoginOrRegisterRequest) error {
err := validation.ValidateStruct(&req,
validation.Field(req.PhoneNumber,
validation.Required,
validation.Match(regexp.MustCompile(PhoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid)),
validation.Field(req.VerifyCode,
validation.Required))
return err
}

11
gamificationapp/app.go Normal file
View File

@ -0,0 +1,11 @@
package gamificationapp
type Application struct {
Config Config
}
func setUp(cnf Config) *Application {
return &Application{
cnf,
}
}

View File

@ -0,0 +1,4 @@
package gamificationapp
type Config struct {
}

View File

@ -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,
}
}

View File

@ -0,0 +1,7 @@
package gamification
import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) {
}

View File

@ -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,
}
}

7
go.mod
View File

@ -9,11 +9,12 @@ require (
github.com/go-sql-driver/mysql v1.9.3
github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
github.com/knadh/koanf v1.5.0
github.com/knadh/koanf/v2 v2.3.0
github.com/labstack/echo-jwt/v4 v4.4.0
github.com/labstack/echo/v4 v4.15.1
github.com/labstack/gommon v0.4.2
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.19
github.com/ory/dockertest/v3 v3.12.0
@ -24,6 +25,8 @@ require (
github.com/swaggo/echo-swagger v1.5.2
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.48.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
@ -57,7 +60,6 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@ -94,5 +96,6 @@ require (
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

34
go.sum
View File

@ -98,6 +98,10 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@ -147,6 +151,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -158,12 +164,14 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
@ -226,8 +234,6 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@ -409,6 +415,18 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@ -530,6 +548,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -538,6 +558,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@ -546,6 +568,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -557,6 +581,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -48,7 +48,7 @@ func MariaDB(cfg config.Config) *mysql.DB {
if *migrate {
migrator.New(migrator.Config{
MysqlConfig: cfg.Mysql,
MigrationPath: "./repository/mysql/migration",
MigrationPath: "./repository/mysql/migrations",
MigrationDBName: "gorp_migrations",
}).Up()
}

View File

@ -3,23 +3,53 @@ package paymentapp
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/payment/gateway"
"git.gocasts.ir/ebhomengo/niki/domain/payment/repository"
"git.gocasts.ir/ebhomengo/niki/domain/payment/service"
"git.gocasts.ir/ebhomengo/niki/paymentapp/delivery/grpc"
"git.gocasts.ir/ebhomengo/niki/pkg/database"
sgrpc "git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type App struct {
paymentrepo service.PaymentRepo
paymentMethodRepo service.PaymentMethodRepo
gwFactory service.GatewayFactory
paymentService *service.PaymentService
paymenthandler *grpc.Handler
rpcServer *sgrpc.RPCServer
server grpc.PaymentGrpcServer
}
func Setup(ctx context.Context, cfg Config, conn *database.Database) (*App, error) {
paymentrepo := repository.NewPaymentRepository(conn.DB)
paymentMethodRepo := repository.NewPaymentMethodRepository(conn.DB)
gwFactory := gateway.NewGatewayFactory()
paymentService := service.NewPaymentService(paymentrepo, paymentMethodRepo, gwFactory)
//TODO Setup
paymenthandler := grpc.NewHandler(paymentService)
return &App{}, nil
rpcServer := sgrpc.New(sgrpc.Config(cfg.Grpc))
server := grpc.NewPaymentGrpcServer(rpcServer, paymenthandler)
return &App{
paymentrepo: paymentrepo,
paymentMethodRepo: paymentMethodRepo,
gwFactory: gwFactory,
paymentService: paymentService,
paymenthandler: paymenthandler,
rpcServer: rpcServer,
server: server,
}, nil
}
func (a *App) Start() error {
a.server.Serve()
return nil
}
func (a *App) Stop(ctx context.Context) error {
return nil
}

View File

@ -1,5 +1,12 @@
package paymentapp
import "time"
type GrpcConfig struct {
Port int `yaml:"port"`
NetworkType string `yaml:"type"`
ShutDownCtxTimeout time.Duration `yaml:"shutdown_context_timeout"`
}
type PostgresConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
@ -19,6 +26,7 @@ type LoggerConfig struct {
}
type Config struct {
Grpc GrpcConfig `yaml:"grpc" json:"grpc"`
Postgres PostgresConfig `yaml:"postgres" json:"postgres"`
Logger LoggerConfig `yaml:"logger" json:"logger"`
}

View File

@ -0,0 +1,42 @@
package grpc
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
"git.gocasts.ir/ebhomengo/niki/domain/payment/service"
paymentpb "git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf"
)
type Handler struct {
paymentpb.UnimplementedPaymentServiceServer
scvPayment *service.PaymentService
}
func NewHandler(s *service.PaymentService) *Handler {
return &Handler{
scvPayment: s,
}
}
func (h *Handler) InitiatePayment(ctx context.Context, req *paymentpb.InitiatePaymentRequest) (*paymentpb.InitiatePaymentResponse, error) {
paymentResponse, err := h.scvPayment.InitiatePayment(ctx, service.InitiatePaymentRequest{
UserID: uint(req.GetUserId()),
PayableType: entity.PayableType(req.GetPayableType()),
PayableID: uint(req.GetPayableId()),
GatewayCode: req.GetGatewayCode(),
CallbackURL: req.GetCallbackUrl(),
Amount: req.GetAmount(),
})
if err != nil {
return nil, err
}
response := &paymentpb.InitiatePaymentResponse{
PaymentId: uint64(paymentResponse.PaymentID),
RedirectUrl: paymentResponse.RedirectURL,
}
return response, nil
}

View File

@ -0,0 +1,37 @@
package grpc
import (
"fmt"
"net"
paymentpb "git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf"
"git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type PaymentGrpcServer struct {
server *grpc.RPCServer
handler *Handler
}
func NewPaymentGrpcServer(server *grpc.RPCServer, handler *Handler) PaymentGrpcServer {
return PaymentGrpcServer{
server: server,
handler: handler,
}
}
func (s PaymentGrpcServer) Serve() error {
listener, err := net.Listen(s.server.Config.NetworkType, fmt.Sprintf(":%d", s.server.Config.Port))
if err != nil {
return err
}
paymentpb.RegisterPaymentServiceServer(s.server.Server, s.handler)
if err := s.server.Server.Serve(listener); err != nil {
return err
}
return nil
}
func (s PaymentGrpcServer) Stop() {
s.server.Stop()
}

View File

@ -0,0 +1,230 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v4.25.1
// source: paymentapp/protobuf/payment.proto
package paymentpb
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type InitiatePaymentRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId uint64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
PayableType string `protobuf:"bytes,2,opt,name=payable_type,json=payableType,proto3" json:"payable_type,omitempty"`
PayableId uint64 `protobuf:"varint,3,opt,name=payable_id,json=payableId,proto3" json:"payable_id,omitempty"`
GatewayCode string `protobuf:"bytes,4,opt,name=gateway_code,json=gatewayCode,proto3" json:"gateway_code,omitempty"`
CallbackUrl string `protobuf:"bytes,5,opt,name=callback_url,json=callbackUrl,proto3" json:"callback_url,omitempty"`
Amount int64 `protobuf:"varint,6,opt,name=amount,proto3" json:"amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InitiatePaymentRequest) Reset() {
*x = InitiatePaymentRequest{}
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InitiatePaymentRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InitiatePaymentRequest) ProtoMessage() {}
func (x *InitiatePaymentRequest) ProtoReflect() protoreflect.Message {
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InitiatePaymentRequest.ProtoReflect.Descriptor instead.
func (*InitiatePaymentRequest) Descriptor() ([]byte, []int) {
return file_paymentapp_protobuf_payment_proto_rawDescGZIP(), []int{0}
}
func (x *InitiatePaymentRequest) GetUserId() uint64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *InitiatePaymentRequest) GetPayableType() string {
if x != nil {
return x.PayableType
}
return ""
}
func (x *InitiatePaymentRequest) GetPayableId() uint64 {
if x != nil {
return x.PayableId
}
return 0
}
func (x *InitiatePaymentRequest) GetGatewayCode() string {
if x != nil {
return x.GatewayCode
}
return ""
}
func (x *InitiatePaymentRequest) GetCallbackUrl() string {
if x != nil {
return x.CallbackUrl
}
return ""
}
func (x *InitiatePaymentRequest) GetAmount() int64 {
if x != nil {
return x.Amount
}
return 0
}
type InitiatePaymentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
PaymentId uint64 `protobuf:"varint,1,opt,name=payment_id,json=paymentId,proto3" json:"payment_id,omitempty"`
RedirectUrl string `protobuf:"bytes,2,opt,name=redirect_url,json=redirectUrl,proto3" json:"redirect_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InitiatePaymentResponse) Reset() {
*x = InitiatePaymentResponse{}
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InitiatePaymentResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InitiatePaymentResponse) ProtoMessage() {}
func (x *InitiatePaymentResponse) ProtoReflect() protoreflect.Message {
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InitiatePaymentResponse.ProtoReflect.Descriptor instead.
func (*InitiatePaymentResponse) Descriptor() ([]byte, []int) {
return file_paymentapp_protobuf_payment_proto_rawDescGZIP(), []int{1}
}
func (x *InitiatePaymentResponse) GetPaymentId() uint64 {
if x != nil {
return x.PaymentId
}
return 0
}
func (x *InitiatePaymentResponse) GetRedirectUrl() string {
if x != nil {
return x.RedirectUrl
}
return ""
}
var File_paymentapp_protobuf_payment_proto protoreflect.FileDescriptor
const file_paymentapp_protobuf_payment_proto_rawDesc = "" +
"\n" +
"!paymentapp/protobuf/payment.proto\x12\apayment\"\xd1\x01\n" +
"\x16InitiatePaymentRequest\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x04R\x06userId\x12!\n" +
"\fpayable_type\x18\x02 \x01(\tR\vpayableType\x12\x1d\n" +
"\n" +
"payable_id\x18\x03 \x01(\x04R\tpayableId\x12!\n" +
"\fgateway_code\x18\x04 \x01(\tR\vgatewayCode\x12!\n" +
"\fcallback_url\x18\x05 \x01(\tR\vcallbackUrl\x12\x16\n" +
"\x06amount\x18\x06 \x01(\x03R\x06amount\"[\n" +
"\x17InitiatePaymentResponse\x12\x1d\n" +
"\n" +
"payment_id\x18\x01 \x01(\x04R\tpaymentId\x12!\n" +
"\fredirect_url\x18\x02 \x01(\tR\vredirectUrl2f\n" +
"\x0ePaymentService\x12T\n" +
"\x0fInitiatePayment\x12\x1f.payment.InitiatePaymentRequest\x1a .payment.InitiatePaymentResponseB=Z;git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf;paymentpbb\x06proto3"
var (
file_paymentapp_protobuf_payment_proto_rawDescOnce sync.Once
file_paymentapp_protobuf_payment_proto_rawDescData []byte
)
func file_paymentapp_protobuf_payment_proto_rawDescGZIP() []byte {
file_paymentapp_protobuf_payment_proto_rawDescOnce.Do(func() {
file_paymentapp_protobuf_payment_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_paymentapp_protobuf_payment_proto_rawDesc), len(file_paymentapp_protobuf_payment_proto_rawDesc)))
})
return file_paymentapp_protobuf_payment_proto_rawDescData
}
var file_paymentapp_protobuf_payment_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_paymentapp_protobuf_payment_proto_goTypes = []any{
(*InitiatePaymentRequest)(nil), // 0: payment.InitiatePaymentRequest
(*InitiatePaymentResponse)(nil), // 1: payment.InitiatePaymentResponse
}
var file_paymentapp_protobuf_payment_proto_depIdxs = []int32{
0, // 0: payment.PaymentService.InitiatePayment:input_type -> payment.InitiatePaymentRequest
1, // 1: payment.PaymentService.InitiatePayment:output_type -> payment.InitiatePaymentResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_paymentapp_protobuf_payment_proto_init() }
func file_paymentapp_protobuf_payment_proto_init() {
if File_paymentapp_protobuf_payment_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_paymentapp_protobuf_payment_proto_rawDesc), len(file_paymentapp_protobuf_payment_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_paymentapp_protobuf_payment_proto_goTypes,
DependencyIndexes: file_paymentapp_protobuf_payment_proto_depIdxs,
MessageInfos: file_paymentapp_protobuf_payment_proto_msgTypes,
}.Build()
File_paymentapp_protobuf_payment_proto = out.File
file_paymentapp_protobuf_payment_proto_goTypes = nil
file_paymentapp_protobuf_payment_proto_depIdxs = nil
}

View File

@ -0,0 +1,25 @@
syntax = "proto3";
package payment;
option go_package = "git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf;paymentpb";
service PaymentService {
rpc InitiatePayment (InitiatePaymentRequest) returns (InitiatePaymentResponse);
}
message InitiatePaymentRequest {
uint64 user_id = 1;
string payable_type = 2;
uint64 payable_id = 3;
string gateway_code = 4;
string callback_url = 5;
int64 amount = 6;
}
message InitiatePaymentResponse {
uint64 payment_id = 1;
string redirect_url = 2;
}

View File

@ -0,0 +1,121 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v4.25.1
// source: paymentapp/protobuf/payment.proto
package paymentpb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
PaymentService_InitiatePayment_FullMethodName = "/payment.PaymentService/InitiatePayment"
)
// PaymentServiceClient is the client API for PaymentService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type PaymentServiceClient interface {
InitiatePayment(ctx context.Context, in *InitiatePaymentRequest, opts ...grpc.CallOption) (*InitiatePaymentResponse, error)
}
type paymentServiceClient struct {
cc grpc.ClientConnInterface
}
func NewPaymentServiceClient(cc grpc.ClientConnInterface) PaymentServiceClient {
return &paymentServiceClient{cc}
}
func (c *paymentServiceClient) InitiatePayment(ctx context.Context, in *InitiatePaymentRequest, opts ...grpc.CallOption) (*InitiatePaymentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InitiatePaymentResponse)
err := c.cc.Invoke(ctx, PaymentService_InitiatePayment_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// PaymentServiceServer is the server API for PaymentService service.
// All implementations must embed UnimplementedPaymentServiceServer
// for forward compatibility.
type PaymentServiceServer interface {
InitiatePayment(context.Context, *InitiatePaymentRequest) (*InitiatePaymentResponse, error)
mustEmbedUnimplementedPaymentServiceServer()
}
// UnimplementedPaymentServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedPaymentServiceServer struct{}
func (UnimplementedPaymentServiceServer) InitiatePayment(context.Context, *InitiatePaymentRequest) (*InitiatePaymentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method InitiatePayment not implemented")
}
func (UnimplementedPaymentServiceServer) mustEmbedUnimplementedPaymentServiceServer() {}
func (UnimplementedPaymentServiceServer) testEmbeddedByValue() {}
// UnsafePaymentServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to PaymentServiceServer will
// result in compilation errors.
type UnsafePaymentServiceServer interface {
mustEmbedUnimplementedPaymentServiceServer()
}
func RegisterPaymentServiceServer(s grpc.ServiceRegistrar, srv PaymentServiceServer) {
// If the following call panics, it indicates UnimplementedPaymentServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&PaymentService_ServiceDesc, srv)
}
func _PaymentService_InitiatePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitiatePaymentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PaymentServiceServer).InitiatePayment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PaymentService_InitiatePayment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PaymentServiceServer).InitiatePayment(ctx, req.(*InitiatePaymentRequest))
}
return interceptor(ctx, in, info, handler)
}
// PaymentService_ServiceDesc is the grpc.ServiceDesc for PaymentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var PaymentService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "payment.PaymentService",
HandlerType: (*PaymentServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "InitiatePayment",
Handler: _PaymentService_InitiatePayment_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "paymentapp/protobuf/payment.proto",
}

View File

@ -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()
}

View File

@ -0,0 +1 @@
package migrator

View File

@ -0,0 +1,9 @@
package postgres
type statementKey uint
const (
StatementKeyAWalletGetTransactionHistory statementKey = iota + 1
StatementKeyWalletInsertTransaction
StatementKeyWalletGetUserWallet
)

View File

@ -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"

25
pkg/grpc/client.go Normal file
View File

@ -0,0 +1,25 @@
package grpc
import (
"context"
"fmt"
"google.golang.org/grpc"
)
type Client struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
}
func NewClient(cfg Client) (*grpc.ClientConn, error) {
address := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
conn, err := grpc.DialContext(context.Background(), address, grpc.WithInsecure())
fmt.Println(err)
if err != nil {
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
}
return conn, nil
}

31
pkg/grpc/server.go Normal file
View File

@ -0,0 +1,31 @@
package grpc
import (
"time"
"google.golang.org/grpc"
)
type Config struct {
Port int `koanf:"port"`
NetworkType string `koanf:"type"`
ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
}
type RPCServer struct {
Config Config
Server *grpc.Server
}
func New(cfg Config) *RPCServer {
grpcServer := grpc.NewServer()
return &RPCServer{
Server: grpcServer,
Config: cfg,
}
}
func (s RPCServer) Stop() {
s.Server.GracefulStop()
}

65
pkg/migrator/migrator.go Normal file
View File

@ -0,0 +1,65 @@
package migrator
import (
"database/sql"
"fmt"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
migrate "github.com/rubenv/sql-migrate"
)
type Migrator struct {
dialect string
dbConfig mysql.Config
migrations *migrate.FileMigrationSource
}
func New(dbConfig mysql.Config, path string) Migrator {
migrations := &migrate.FileMigrationSource{
Dir: path,
}
return Migrator{
dialect: "mysql",
dbConfig: dbConfig,
migrations: migrations,
}
}
func (m Migrator) Up() {
db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
m.dbConfig.Username,
m.dbConfig.Password,
m.dbConfig.Host,
m.dbConfig.Port,
m.dbConfig.DBName))
if err != nil {
fmt.Println(err)
panic(fmt.Errorf("can't open db : %v", err))
}
n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Up)
if err != nil {
panic(fmt.Errorf("can't apply migrations: %v", err))
}
fmt.Printf("Applied %d migrations!\n", n)
}
func (m Migrator) Down() {
db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
m.dbConfig.Username,
m.dbConfig.Password,
m.dbConfig.Host,
m.dbConfig.Port,
m.dbConfig.DBName))
if err != nil {
panic(fmt.Errorf("can't open db connection: %v", err))
}
n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Down)
if err != nil {
panic(fmt.Errorf("can't rollback migrations: %v", err))
}
fmt.Printf("Rollback %d migrations!\n", n)
}

3
pkg/types/id.go Normal file
View File

@ -0,0 +1,3 @@
package types
type ID uint64

Some files were not shown because too many files have changed in this diff Show More