Compare commits

..

3 Commits

949 changed files with 20215 additions and 434565 deletions

View File

@ -1,34 +0,0 @@
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

4
.gitignore vendored
View File

@ -29,6 +29,4 @@ tmp
logs/ logs/
mise.log mise.log
curl curl
cmd/**/temp/main

View File

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

126
Makefile
View File

@ -1,64 +1,54 @@
# --- Variables --- # TODO: add commands for build and run in dev/produciton mode
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
start: build .PHONY: help confirm lint test format build run docker swagger watch migrate/status migrate/new migrate/up migrate/down
$(BUILD_DIR)/$(BINARY_NAME)
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)/...
test: test:
go test -v ./... 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: format:
@which gofumpt || (go install mvdan.cc/gofumpt@latest) @which gofumpt || (go install mvdan.cc/gofumpt@latest)
@gofumpt -l -w . @gofumpt -l -w $(ROOT)
@which gci || (go install github.com/daixiang0/gci@latest) @which gci || (go install github.com/daixiang0/gci@latest)
@gci write . --skip-generated --skip-vendor @gci write $(ROOT) --skip-generated --skip-vendor
@which golangci-lint || (go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.0)
@golangci-lint run --fix @golangci-lint run --fix
build:
go build -o niki main.go
run:
go run main.go --migrate
docker:
sudo docker compose up -d
swagger: swagger:
swag init swag init
# Live Reload # Live Reload
watch: watch:
@if command -v CompileDaemon > /dev/null; then \ @if command -v CompileDaemon > /dev/null; then \
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \ CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./niki"; \
else \ else \
read -p "Go's 'CompileDaemon' is not installed. Do you want to install it? [Y/n] " choice; \ read -p "Go's 'CompileDaemon' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
go install github.com/githubnemo/CompileDaemon@latest; \ go install github.com/githubnemo/CompileDaemon@latest; \
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \ CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./niki"; \
else \ else \
echo "You chose not to install CompileDaemon. Exiting..."; \ echo "You chose not to install CompileDaemon. Exiting..."; \
exit 1; \ exit 1; \
fi; \ fi; \
fi fi
# ====================================================================================
# Database Migration Commands (legacy niki-core)
# ====================================================================================
.PHONY: migrate/status migrate/new migrate/up migrate/down
migrate/status: migrate/status:
@sql-migrate status -env="production" -config=repository/mysql/dbconfig.yml @sql-migrate status -env="production" -config=repository/mysql/dbconfig.yml
@ -73,68 +63,4 @@ migrate/up: confirm
migrate/down: confirm migrate/down: confirm
@echo 'Tearing down last migration...' @echo 'Tearing down last migration...'
@sql-migrate down -env="production" -config=repository/mysql/dbconfig.yml -limit=1 @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"

View File

@ -1,44 +0,0 @@
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)
}
}

View File

@ -1,19 +0,0 @@
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

@ -1,68 +0,0 @@
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
}

View File

@ -1,53 +0,0 @@
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
}

7
agentapp/app.go Normal file
View File

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

7
agentapp/config.go Normal file
View File

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

View File

@ -0,0 +1 @@
package http

View File

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

View File

@ -0,0 +1 @@
package repository

View File

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

View File

@ -0,0 +1 @@
package service

View File

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

View File

@ -0,0 +1 @@
package service

View File

@ -1,23 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -1,14 +0,0 @@
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() {}

View File

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

View File

@ -1,52 +0,0 @@
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,21 +0,0 @@
package command
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "payment",
Short: "Payment service",
Long: "Payment service CLI",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -1,122 +0,0 @@
package command
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
paymentapp "git.gocasts.ir/ebhomengo/niki/paymentapp"
"git.gocasts.ir/ebhomengo/niki/pkg/database"
)
var configPath string
var serveCmd = &cobra.Command{
Use: "serve",
Short: "start payment service",
RunE: startServer,
}
func init() {
serveCmd.Flags().StringVar(
&configPath,
"config",
"deploy/payment/development/config.yaml",
"config file path",
)
rootCmd.AddCommand(serveCmd)
}
func startServer(cmd *cobra.Command, args []string) error {
// -------------------------
// load config
// -------------------------
//TODO --chage to Loader
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("🚩 read config error: %w", err)
}
var cfg paymentapp.Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("🚩 parse config error: %w", err)
}
// -------------------------
// connect database
// -------------------------
dbConn, err := database.Connect(database.Config{
Port: cfg.Postgres.Port,
Host: cfg.Postgres.Host,
Username: cfg.Postgres.User,
DBName: cfg.Postgres.DbName,
Password: cfg.Postgres.Password,
Driver: cfg.Postgres.Driver,
SSLMode: cfg.Postgres.SSLMode,
MaxIdleConns: cfg.Postgres.MaxIdleConns,
MaxOpenConns: cfg.Postgres.MaxOpenConns,
ConnMaxLifetime: cfg.Postgres.ConnMaxLifetime,
})
if err != nil {
return err
}
defer database.Close(dbConn.DB)
// -------------------------
// context
// -------------------------
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// -------------------------
// setup app
// -------------------------
app, err := paymentapp.Setup(ctx, cfg, dbConn)
if err != nil {
return err
}
// -------------------------
// start server
// -------------------------
go func() {
if err := app.Start(); err != nil {
fmt.Println("🚩 server error:", err)
cancel()
}
}()
fmt.Println("payment service started 🏃‍➡️")
// -------------------------
// shutdown
// -------------------------
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("shutting down 🥱...")
shutdownCtx, shutdownCancel := context.WithTimeout(
context.Background(),
10*time.Second,
)
defer shutdownCancel()
return app.Stop(shutdownCtx)
}

View File

@ -1,7 +0,0 @@
package main
import "git.gocasts.ir/ebhomengo/niki/cmd/payment/command"
func main() {
command.Execute()
}

View File

@ -1,16 +1,13 @@
package command package command
import ( import (
"context" "fmt"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"time"
"git.gocasts.ir/ebhomengo/niki/productapp"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,53 +25,27 @@ var serveCmd = &cobra.Command{
func serve() { func serve() {
log.Println("Product Service Starting...") log.Println("Product Service Starting...")
cfg := productapp.Config{ // TODO: Initialize database connection
HTTPServer: productapp.HTTPServerConfig{ // TODO: Initialize service dependencies
Port: getEnvInt("HTTP_PORT", 8080), // TODO: Setup HTTP server with routes
},
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() { go func() {
app.Start() sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down Product Service gracefully...")
os.Exit(0)
}() }()
quit := make(chan os.Signal, 1) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) fmt.Fprintf(w, "Product Service OK!")
<-quit })
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) log.Printf("Product Service listening on port %s", port)
defer cancel() if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Failed to start server: %v", err)
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() { func init() {

View File

@ -1,7 +1,63 @@
package main package main
import (
"flag"
"fmt"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http"
purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
"git.gocasts.ir/ebhomengo/niki/repository/migrator"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
func MariaDB() *mysql.DB {
cfg := mysql.Config{
Username: "niki",
Password: "nikiappt0lk2o20",
Port: 3306,
Host: "localhost",
DBName: "niki_db",
}
migrate := flag.Bool("migrate", false, "perform database migration")
flag.Parse()
if *migrate {
migrator.New(migrator.Config{
MysqlConfig: cfg,
MigrationPath: "./purchaseapp/repository/mysql/migration",
MigrationDBName: "gorp_migrations",
}).Up()
}
return mysql.New(cfg)
}
func main() { func main() {
cfg := mysql.Config{
Username: "niki",
Password: "nikiappt0lk2o20",
Port: 3306,
Host: "localhost",
DBName: "niki_db",
}
db := mysql.New(cfg)
defer func() {
if err := db.CloseStatements(); err != nil {
fmt.Printf("Error closing statements: %v\n", err)
}
}()
orderRepo := purchaseMysql.New(db)
orderSvc := Service(orderRepo)
server := HTTPServer(orderSvc)
server.Serve()
} }
func HTTPServer(orderSvc order.Service) *http.Server {
return http.New(orderSvc)
}
func Service(orderRepo *purchaseMysql.DB) order.Service {
return order.New(orderRepo)
}

View File

@ -1 +0,0 @@
package command

View File

@ -1,52 +0,0 @@
package command
import (
cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader"
"git.gocasts.ir/ebhomengo/niki/pkg/path"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp"
"github.com/spf13/cobra"
"log"
"os"
"path/filepath"
)
var RootCmd = &cobra.Command{
Use: "shoppingbasket_service",
Short: "A CLI for shoppingbasket service",
Long: `shoppingbasket Service CLI is a tool to manage and run
the shoppingbasket service, including migrations and server startup.`,
}
func loadAppConfig() shoppingbasketapp.Config {
var cfg shoppingbasketapp.Config
projectRoot, err := path.PathProjectRoot()
if err != nil {
log.Fatalf("error finding project root: %v", err)
}
yamlPath := os.Getenv("CONFIG_PATH")
if yamlPath == "" {
defaultConfig := filepath.Join(projectRoot, "deploy", "shoppingbasket", "development", "config.yml")
if _, err := os.Stat(defaultConfig); err == nil {
yamlPath = defaultConfig
} else {
yamlPath = filepath.Join(projectRoot, "deploy", "shoppingbasket", "development", "config.local.yml")
}
}
options := cfgloader.Option{
Prefix: "SHOPPINGBASKET_",
Delimiter: ".",
Separator: "__",
YamlFilePath: yamlPath,
CallbackEnv: nil,
}
if err := cfgloader.Load(options, &cfg); err != nil {
log.Fatalf("Failed to load benefactor config: %v", err)
}
return cfg
}

View File

@ -1,43 +0,0 @@
package command
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp"
"github.com/labstack/gommon/log"
"github.com/spf13/cobra"
)
var ServeCmd = &cobra.Command{
Use: "serve",
Short: "Start shoppingbasket service",
Long: `This command starts the main shoppingbasket service.`,
Run: func(cmd *cobra.Command, args []string) {
},
}
func serve() {
var cfg = loadAppConfig()
logger.Init(cfg.Logger)
l := logger.L()
l.Info("Starting shoppingbasket service...")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app, err := shoppingbasketapp.Setup(ctx, cfg)
if err != nil {
l.Error("failed initialize shopping basket app", "error", err)
log.Fatalf(fmt.Sprintf("error starting shopping basket app: %v", err))
}
app.Start()
}
func init() {
RootCmd.AddCommand(ServeCmd)
}

View File

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

View File

@ -1,281 +0,0 @@
// 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

@ -1,159 +0,0 @@
// 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

@ -1,28 +0,0 @@
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() { func MigrateMariaDB(cfg mysql.Config) func() {
migrations := migrator.New(migrator.Config{ migrations := migrator.New(migrator.Config{
MysqlConfig: cfg, MysqlConfig: cfg,
MigrationPath: "../../../repository/mysql/migrations", MigrationPath: "../../../repository/mysql/migration",
MigrationDBName: "gorp_migrations", MigrationDBName: "gorp_migrations",
}) })
migrations.Up() migrations.Up()

View File

@ -1,27 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,19 +0,0 @@
http:
addr: ":9090" #--
host: "localhost"
port: 9090
read_timeout: 5
write_timeout: 10
idle_timeout: 60
postgres:
host: "localhost"
port: 5432
driver: "postgres"
user: h1user
password: h1pass
dbName: h1db
sslMode: disable
maxIdleConns: 15
maxOpenConns: 100
connMaxLifetime: 5

View File

@ -1,27 +0,0 @@
services:
app:
container_name: h1-app
build: .
ports:
- "9090:9090"
depends_on:
- postgres
environment:
CONFIG_PATH: "/app/config.yaml"
volumes:
- ./config.yaml:/app/config.yaml
postgres:
image: postgres:15
container_name: h1-postgres
environment:
POSTGRES_USER: h1user
POSTGRES_PASSWORD: h1pass
POSTGRES_DB: h1db
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:

View File

@ -1,19 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,43 +0,0 @@
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

@ -1,25 +0,0 @@
redis:
host: "localhost"
port: 6379
password: ""
db: 0
repo:
kart_key_prefix: "shopping-basket-cart:"
ttl: 3600s
http_server:
host: "localhost"
port: 8080
shutdown_context_timeout: 10s
cors:
allow_origins:
- "*"
logger:
level: "debug" # Can be `debug`, `info`, `warn`, `error`
file_path: "logs/shoppingbasketapp/service.log"
use_local_time: true
file_max_size_in_mb: 10
file_max_age_in_days: 7

View File

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

View File

@ -1,87 +0,0 @@
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

@ -1,13 +0,0 @@
-- +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

@ -1,67 +0,0 @@
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

@ -1,115 +0,0 @@
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

@ -1,19 +0,0 @@
package entity
import (
"time"
"git.gocasts.ir/ebhomengo/niki/pkg/types"
)
type Benefactor struct {
ID types.ID
FirstName string
LastName string
PhoneNumber string
Description string
Email string
Gender Gender
BirthDate time.Time
Status BenefactorStatus
}

View File

@ -1,19 +0,0 @@
package entity
type BenefactorStatus string
const (
BenefactorActiveStatus = BenefactorStatus("active")
BenefactorInactiveStatus = BenefactorStatus("inactive")
)
var BenefactorStatusStrings = map[BenefactorStatus]string{
BenefactorActiveStatus: "active",
BenefactorInactiveStatus: "inactive",
}
func (b BenefactorStatus) IsValid() bool {
_, ok := BenefactorStatusStrings[b]
return ok
}

View File

@ -1,19 +0,0 @@
package entity
type Gender string
const (
MaleGender = Gender("male")
FemaleGender = Gender("female")
)
var GenderStrings = map[Gender]string{
MaleGender: "male",
FemaleGender: "female",
}
func (g Gender) IsValid() bool {
_, ok := GenderStrings[g]
return ok
}

View File

@ -1,86 +0,0 @@
package mysql
import (
"context"
"database/sql"
"git.gocasts.ir/ebhomengo/niki/domain/benefactor/entity"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/pkg/types"
)
func (d *DB) Create(ctx context.Context, b entity.Benefactor) (entity.Benefactor, error) {
const op = "repository.mysql.benefactor.create"
query := `INSERT INTO benefactors
(first_name, last_name, phone_number, description, email, gender, birthdate)
VALUES(?, ?, ?, ?, ?, ?, ?)`
res, err := d.conn.Conn().ExecContext(ctx, query,
b.FirstName, b.LastName, b.PhoneNumber,b.Description, b.Email, b.Gender, b.BirthDate)
if err != nil {
return entity.Benefactor{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgSomethingWentWrong).WithKind(richerror.KindUnexpected)
}
id, _ := res.LastInsertId()
b.ID = types.ID(id)
return b, nil
}
func (d *DB) GetBenefactorByID(ctx context.Context, benefactorID types.ID) (entity.Benefactor, error) {
const op = "repository.mysql.benefactor.getBenefactorById"
var b entity.Benefactor
query := `SELECT * FROM benefactors WHERE id = ?`
row := d.conn.Conn().QueryRowContext(ctx, query, benefactorID)
err := row.Scan(&b.ID, &b.FirstName, &b.LastName, &b.PhoneNumber,
&b.Description, &b.Email, b.Gender, b.BirthDate, b.Status)
if err != nil {
if err == sql.ErrNoRows {
return entity.Benefactor{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindNotFound)
}
return entity.Benefactor{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
}
return b, nil
}
func (d *DB) Activate(ctx context.Context, benefactorID types.ID) error {
const op = "repository.mysql.benefactor.Activate"
query := `UPDATE benefactors SET status ='active' WHERE id = ?`
_, err := d.conn.Conn().ExecContext(ctx, query, benefactorID)
if err != nil {
return richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgSomethingWentWrong).WithKind(richerror.KindUnexpected)
}
return nil
}
func (d *DB) Deactivate(ctx context.Context, benefactorID types.ID) error {
const op = "repository.mysql.benefactor.Deativate"
query := `UPDATE benefactors SET status ='inactive' WHERE id = ?`
_, err := d.conn.Conn().ExecContext(ctx, query, benefactorID)
if err != nil {
return richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgSomethingWentWrong).WithKind(richerror.KindUnexpected)
}
return nil
}

View File

@ -1,13 +0,0 @@
package mysql
import(
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
type DB struct {
conn *mysql.DB
}
func New(conn *mysql.DB) *DB {
return &DB{conn: conn}
}

View File

@ -1,18 +0,0 @@
-- +migrate Up
CREATE TABLE `benefactors` (
`id` INT NOT NULL PRIMARY KEY,
`first_name` VARCHAR(100) NOT NULL,
`last_name` VARCHAR(100) NOT NULL,
`phone_number` VARCHAR(20) NOT NULL,
`description` TEXT,
`email` VARCHAR(255),
`gender` ENUM('male', 'female') NOT NULL,
`birth_date` DATE,
`status` ENUM('active', 'inactive') NOT NULL DEFAULT `active`,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +migrate Down
DROP TABLE `benefactors`;

View File

@ -1,36 +0,0 @@
package service
import (
"time"
"git.gocasts.ir/ebhomengo/niki/domain/benefactor/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type CreateBenefactorRequest struct {
ID types.ID `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
Description string `json:"description"`
Email string `json:"email"`
Gender entity.Gender `json:"gender"`
BirthDate time.Time `json:"birth_date"`
}
type CreateBenefactorResponse struct {
Name string `json:"name"`
Email string `json:"email"`
}
type ProfileRequest struct {
BenefactorID types.ID
}
type ProfileResponse struct {
Name string `json:"name"`
}
type ActivenessRequest struct {
BenefactorID types.ID
}

View File

@ -1,87 +0,0 @@
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/benefactor/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/pkg/types"
)
type Repository interface {
Create(ctx context.Context, b entity.Benefactor) (entity.Benefactor, error)
GetBenefactorByID(ctx context.Context, benefactorID types.ID) (entity.Benefactor, error)
Activate(ctx context.Context, benefactorID types.ID) error
Deactivate(ctx context.Context, benefactorID types.ID) error
}
type Service struct {
repo Repository
}
func New(repo Repository) Service {
return Service{repo: repo}
}
func (s Service) CreateBenefactor(ctx context.Context, req CreateBenefactorRequest) (CreateBenefactorResponse, error) {
const op = "beneafactorservice.CreateBenefactor"
benefactor := entity.Benefactor{
ID: 0,
FirstName: req.FirstName,
LastName: req.LastName,
PhoneNumber: req.PhoneNumber,
Description: req.Description,
Email: req.Email,
Gender: req.Gender,
BirthDate: req.BirthDate,
Status: entity.BenefactorActiveStatus,
}
createdBenefactor, err := s.repo.Create(ctx, benefactor)
if err != nil {
return CreateBenefactorResponse{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected)
}
return CreateBenefactorResponse{
Name: createdBenefactor.FirstName + " " + createdBenefactor.LastName,
Email: createdBenefactor.Email,
}, nil
}
func(s Service) Profile(ctx context.Context, req ProfileRequest) (ProfileResponse, error) {
const op = "benefactorservice.Profile"
benefactor, err := s.repo.GetBenefactorByID(ctx, types.ID(req.BenefactorID))
if err != nil {
return ProfileResponse{}, richerror.New(op).WithErr(err).
WithMeta(map[string]interface{}{"req": req})
}
return ProfileResponse{Name: benefactor.FirstName + " " + benefactor.LastName}, nil
}
func (s Service) Activate(ctx context.Context, req ActivenessRequest) error {
const op = "benefactorservice.Activate"
err := s.repo.Activate(ctx, types.ID(req.BenefactorID))
if err != nil {
return richerror.New(op).WithErr(err).
WithKind(richerror.KindUnexpected)
}
return nil
}
func (s Service) Dectivate(ctx context.Context, req ActivenessRequest) error {
const op = "benefactorservice.Deactivate"
err := s.repo.Deactivate(ctx, types.ID(req.BenefactorID))
if err != nil {
return richerror.New(op).WithErr(err).
WithKind(richerror.KindUnexpected)
}
return nil
}

View File

@ -1,41 +0,0 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Campaign struct {
ID types.ID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Link string `json:"link"`
Slogan string `json:"slogan"` //
GoalAmount float64 `json:"goal_amount"`
RaisedAmount float64 `json:"raised_amount"`
Status types.CampaignStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
DeadlineAt *time.Time `json:"deadline_at,omitempty"`
AdminID types.ID `json:"creator_id"`
}
// Behavior
func (c *Campaign) Activate() {
if c.Status == types.CampaignDraft {
c.Status = types.CampaignActive
}
}
func (c *Campaign) AddFunds(amount float64) {
c.RaisedAmount += amount
if c.RaisedAmount >= c.GoalAmount {
c.Status = types.CampaignFinished
}
}
func (c *Campaign) IsExpired(now time.Time) bool {
if c.DeadlineAt == nil {
return false
}
return now.After(*c.DeadlineAt)
}

View File

@ -1,60 +0,0 @@
package mysql
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/campaign/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
"git.gocasts.ir/ebhomengo/niki/types"
)
type DB struct {
conn *mysql.DB
}
func New(db *mysql.DB) *DB {
return &DB{conn: db}
}
// CreateCampaign creates a new campaign
func (d *DB) Create(ctx context.Context, campaign entity.Campaign) (types.ID, error) {
const Op = "repository.mysql.campaign.create"
tx, err := d.conn.Conn().BeginTx(ctx, nil)
if err != nil {
return 0, richerror.New(Op).WithErr(err)
}
defer tx.Rollback()
query := `INSERT INTO campaigns (title, description,link, slogan ,
goal_amount, raised_amount,
status, deadline_at ,admin_id , created_at )
VALUES (?, ?, ?, ?, ?, ?, ? , NOW() )`
result, err := tx.ExecContext(ctx, query,
campaign.Title,
campaign.Description,
campaign.Link,
campaign.Slogan,
campaign.GoalAmount,
campaign.RaisedAmount,
campaign.Status,
campaign.DeadlineAt,
campaign.AdminID,
campaign.CreatedAt,
)
if err != nil {
return 0, richerror.New(Op).WithErr(err)
}
campaignID, err := result.LastInsertId()
if err != nil {
return 0, richerror.New(Op).WithErr(err)
}
if err := tx.Commit(); err != nil {
return 0, richerror.New(Op).WithErr(err)
}
return types.ID(campaignID), nil
}

View File

@ -1,77 +0,0 @@
package service
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/campaign/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
func ToCampaignEntity(req CreateCampaignRequest) entity.Campaign {
return entity.Campaign{
Title: req.Title,
Description: req.Description,
Link: req.Link,
Slogan: req.Slogan,
GoalAmount: req.GoalAmount,
RaisedAmount: 0,
Status: types.CampaignStatus(req.Status),
DeadlineAt: req.DeadlineAt,
AdminID: req.AdminID,
CreatedAt: time.Now(),
}
}
// CreateCampaign handles creation of a new campaign.
func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (types.ID, error) {
const op = "service.campaign.create_campaign"
if err := validateCreateCampaignRequest(req); err != nil {
return 0, richerror.New(op).WithErr(err)
}
campaign := ToCampaignEntity(req)
id, err := s.repo.Create(ctx, campaign)
if err != nil {
return 0, richerror.New(op).WithErr(err)
}
return id, nil
}
func validateCreateCampaignRequest(req CreateCampaignRequest) error {
if req.Title == "" {
return errRequired("title")
}
if req.GoalAmount <= 0 {
return errInvalid("goal_amount must be greater than 0")
}
if req.AdminID == 0 {
return errRequired("admin_id")
}
validStatuses := map[string]bool{
"draft": true,
"active": true,
"completed": true,
"cancelled": true,
"paused": true,
}
if !validStatuses[string(req.Status)] {
return errInvalid("invalid status provided")
}
return nil
}
// --- Helpers ---
func errRequired(field string) error {
return fmt.Errorf("%s is required", field)
}
func errInvalid(msg string) error {
return fmt.Errorf(msg)
}

View File

@ -1,30 +0,0 @@
package service
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type GetCampaignResponse struct {
ID types.ID `json:"campaign_id"`
}
type CreateCampaignRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Link string `json:"link"`
Slogan string `json:"slogan" validate:"max=255"`
GoalAmount float64 `json:"goal_amount"`
Status string `json:"status,omitempty"`
DeadlineAt *time.Time `json:"deadline_at,omitempty"`
AdminID types.ID `json:"admin_id" validate:"required"`
}
type CompletedCampaignResponse struct {
TotalChecked uint64 `json:"total_checked"`
TotalFinished uint64 `json:"total_finished"`
}
type FilterRequest struct {
Limit uint32 `json:"total_checked"`
}

View File

@ -1,40 +0,0 @@
package service
import (
"context"
_ "fmt"
"git.gocasts.ir/ebhomengo/niki/domain/campaign/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type CampaignFilterParam struct {
AdminID types.ID
Status string
//nil true false
IsArchived *bool
}
type CampaignStatus interface {
FindActiveCampaigns(ctx context.Context) ([]entity.Campaign, error)
UpdateStatus(ctx context.Context, id types.ID, status types.CampaignStatus) error
}
type CampaignStorage interface {
Create(ctx context.Context, c entity.Campaign) (types.ID, error)
Update(ctx context.Context, c entity.Campaign) error
FindByID(ctx context.Context, id types.ID) (entity.Campaign, error)
FindAll(ctx context.Context, filter CampaignFilterParam) ([]entity.Campaign, error)
Archive(ctx context.Context, id types.ID) error // instead Delete
TotalDonations(ctx context.Context, campaignID types.ID) (int64, error)
}
type CampaignService struct {
repo CampaignStorage
repoStatus CampaignStatus
}
func NewCampaignService(storage CampaignStorage) *CampaignService {
return &CampaignService{
repo: storage,
}
}

View File

@ -1,64 +0,0 @@
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
func (s *CampaignService) MonitorCampaignProgress(ctx context.Context, req FilterRequest) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.CheckAndCompleteCampaigns(ctx, req)
case <-ctx.Done():
return
}
}
}
func (s *CampaignService) CheckAndCompleteCampaigns(ctx context.Context, req FilterRequest) (CompletedCampaignResponse, error) {
now := time.Now()
//TODO:with filter request later complete
activeCampaigns, err := s.repoStatus.FindActiveCampaigns(ctx)
if err != nil {
return CompletedCampaignResponse{}, err
}
var totalChecked uint64
var totalFinished uint64
for _, campaign := range activeCampaigns {
totalChecked++
shouldFinish := false
if campaign.DeadlineAt != nil && campaign.DeadlineAt.Before(now) {
shouldFinish = true
}
if campaign.RaisedAmount >= campaign.GoalAmount {
shouldFinish = true
}
if shouldFinish && campaign.Status != types.CampaignFinished {
if err := s.repoStatus.UpdateStatus(ctx, campaign.ID, types.CampaignFinished); err != nil {
continue
}
totalFinished++
}
}
return CompletedCampaignResponse{
TotalChecked: totalChecked,
TotalFinished: totalFinished,
}, nil
}

View File

@ -1,11 +0,0 @@
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

@ -1,49 +0,0 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Order struct {
ID types.ID
UserID types.ID
TotalAmount types.Price
TotalDiscount types.Price
ShippingID types.ID
PaymentMethod PaymentMethod
ProcessStatus ProcessStatus
PaymentStatus PaymentStatus
AddressID types.ID
CreatedAt time.Time
UpdatedAt time.Time
}
type PaymentMethod string
const (
Online PaymentMethod = "online"
Wallet = "wallet"
Cart = "cart"
)
type ProcessStatus string
const (
WaitingToPay ProcessStatus = "waiting-to-pay"
Processing = "processing"
Accepted = "accepted"
Preparing = "preparing"
Prepared = "prepared"
GivenToPost = "given-to-post"
Delivered = "delivered"
Cancelled = "cancelled"
SystemCancellation = "system-cancellation"
)
type PaymentStatus string
const (
Paid PaymentStatus = "paid"
UnPaid = "unpaid"
)

View File

@ -1,16 +0,0 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type OrderItem struct {
ID types.ID
ProductID types.ID
Price types.Price
Quantity types.Count
PriceWithDiscount types.Price
OrderID types.ID
CreatedAt time.Time
}

View File

@ -1,10 +0,0 @@
package entity
import "git.gocasts.ir/ebhomengo/niki/types"
type Shipping struct {
ID types.ID
Name string
Price types.Price
IsActive bool
}

View File

@ -1,16 +0,0 @@
-- +migrate Up
-- please read this article to understand why we use VARCHAR(191)
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
CREATE TABLE `orders` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR (191),
`price` INT NOT NULL ,
`is_active` INT NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +migrate Down
DROP TABLE `orders`;

View File

@ -1,11 +0,0 @@
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

@ -1,16 +0,0 @@
package service
import (
"git.gocasts.ir/ebhomengo/niki/domain/order/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (s *Service) GetShipping() ([]entity.Shipping, error) {
const Op = "domain.order.service.shipping.get-shipping"
shippings, err := s.repo.GetShipping()
if err != nil {
return []entity.Shipping{}, richerror.New(Op)
}
return shippings, nil
}

View File

@ -1,16 +0,0 @@
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

@ -1,45 +0,0 @@
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

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

View File

@ -1,35 +0,0 @@
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

@ -1,24 +0,0 @@
package gateway
import (
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/payment/service"
)
type gatewayFactoryImpl struct{}
func NewGatewayFactory() service.GatewayFactory {
return &gatewayFactoryImpl{}
}
func (f gatewayFactoryImpl) GetGatewayAdapter(code string) (service.GatewayPort, error) {
switch code {
case "melat":
return nil, nil
case "zarinpal":
return nil, nil
default:
return nil, fmt.Errorf("Gateway Code(%s) is wrong!", code)
}
}

View File

@ -1 +0,0 @@
package gateway

View File

@ -1,33 +0,0 @@
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

@ -1,23 +0,0 @@
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 +0,0 @@
package database

View File

@ -1,17 +0,0 @@
package service
import "git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
type InitiatePaymentRequest struct {
UserID uint
PayableType entity.PayableType
PayableID uint
GatewayCode string
CallbackURL string
Amount int64
}
type InitiatePaymentResponse struct {
PaymentID uint
RedirectURL string
}

View File

@ -1,105 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentService struct {
paymentRepo PaymentRepo
paymentMethodRepo PaymentMethodRepo
gwFactory GatewayFactory
}
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 {
GetGatewayAdapter(code string) (GatewayPort, error)
}
type GatewayPort interface {
Request(amount int64, callbackURL string, description string) (token string, redirectURL string, rawReq []byte, rawRes []byte, err error)
}
func NewPaymentService(pr PaymentRepo, pmr PaymentMethodRepo, gwf GatewayFactory) *PaymentService {
return &PaymentService{
paymentRepo: pr,
paymentMethodRepo: pmr,
gwFactory: gwf,
}
}
func (s *PaymentService) InitiatePayment(ctx context.Context, req InitiatePaymentRequest) (*InitiatePaymentResponse, error) {
gateway, err := s.paymentMethodRepo.GetGatewayByCode(ctx, req.GatewayCode)
if err != nil || !gateway.IsActive {
return nil, errors.New("gateway is not available")
}
payment := &entity.Payment{
UserID: req.UserID,
PayableType: req.PayableType,
PayableID: req.PayableID,
GatewayID: gateway.ID,
TotalAmount: req.Amount,
Currency: entity.CurrencyIRR,
Status: entity.PaymentStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.paymentRepo.CreatePayment(ctx, payment); err != nil {
return nil, fmt.Errorf("failed to create payment: %w", err)
}
transaction := &entity.PaymentTransaction{
PaymentID: payment.ID,
Type: entity.TransactionTypeRequest,
Status: entity.TransactionStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.paymentRepo.CreateTransaction(ctx, transaction); err != nil {
return nil, fmt.Errorf("failed to create initial transaction: %w", err)
}
gwAdapter, err := s.gwFactory.GetGatewayAdapter(gateway.Code)
if err != nil {
return nil, fmt.Errorf("failed to load gateway adapter: %w", err)
}
desc := fmt.Sprintf(" پرداخت برای %s شماره %d", req.PayableType, req.PayableID)
token, redirectURL, rawReq, rawRes, gwErr := gwAdapter.Request(req.Amount, req.CallbackURL, desc)
transaction.RequestData = rawReq
transaction.ResponseData = rawRes
transaction.UpdatedAt = time.Now()
if gwErr != nil {
transaction.Status = entity.TransactionStatusFailed
transaction.ErrorMessage = gwErr.Error()
_ = s.paymentRepo.UpdateTransaction(ctx, transaction)
return nil, fmt.Errorf("gateway request failed: %w", gwErr)
}
transaction.Status = entity.TransactionStatusSuccess
transaction.GatewayToken = token
if err := s.paymentRepo.UpdateTransaction(ctx, transaction); err != nil {
return nil, fmt.Errorf("failed to update transaction with token: %w", err)
}
return &InitiatePaymentResponse{
PaymentID: payment.ID,
RedirectURL: redirectURL,
}, nil
}

View File

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

View File

@ -1,21 +0,0 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
)
type Item struct {
ProductID types.ID
Quantity int
Price types.Price
Name string
AddedAt int64
}
type Cart struct {
UserID types.ID
Items []Item
TotalPrice types.Price
ExpireAt int64
CreatedAt int64
}

View File

@ -1,272 +0,0 @@
package repository
import (
"context"
"encoding/json"
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/redis/go-redis/v9"
"strconv"
"strings"
"time"
)
const (
FieldNumber = 5
UserIDField = "user_id"
CreatedAtField = "created_at"
ExpireAtField = "expire_at"
TotalPriceField = "total_price"
)
type Config struct {
KartKeyPrefix string `koanf:"kart_key_prefix"`
TTL time.Duration `koanf:"ttl"`
}
type Repo struct {
client *redis.Client
config Config
}
func New(client *redis.Client, cfg Config) Repo {
return Repo{client: client, config: cfg}
}
func (r Repo) cartKey(userID types.ID) string {
return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID)
}
func (r Repo) itemKey(productID types.ID) string {
return fmt.Sprintf("item:%d", productID)
}
func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error {
const op = "shoppingbasketapp.repository.AddItem"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(item.ProductID)
now := time.Now().UnixNano()
itemJson, _ := json.Marshal(item)
exists, _ := r.client.Exists(ctx, cartKey).Result()
if exists == 0 {
r.client.HSet(ctx, cartKey, map[string]interface{}{
UserIDField: userID,
itemKey: string(itemJson),
TotalPriceField: item.Price * types.Price(item.Quantity),
CreatedAtField: now,
ExpireAtField: now + r.config.TTL.Nanoseconds(),
})
} else {
existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result()
if existsItem != "" {
var i entity.Item
if err := json.Unmarshal([]byte(existsItem), &i); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
item.Quantity += i.Quantity
itemJson, _ = json.Marshal(item)
}
r.client.HSet(ctx, cartKey, itemKey, string(itemJson))
r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.TTL.Nanoseconds())
if err := r.updateTotalPrice(ctx, cartKey); err != nil {
return err
}
}
r.client.Expire(ctx, cartKey, r.config.TTL)
return nil
}
func parsInt(s string) int64 {
i, _ := strconv.ParseInt(s, 10, 64)
return i
}
func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
const op = "shoppingbasketapp.repository.GetCart"
cartKey := r.cartKey(userID)
exists, err := r.client.Exists(ctx, cartKey).Result()
if err != nil {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if exists == 0 {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
}
allCart, err := r.client.HGetAll(ctx, cartKey).Result()
if err != nil {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c := entity.Cart{Items: []entity.Item{}}
for field, value := range allCart {
if strings.HasPrefix(field, "item:") {
var i entity.Item
if err := json.Unmarshal([]byte(value), &i); err != nil {
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c.Items = append(c.Items, i)
continue
}
switch field {
case UserIDField:
c.UserID = types.ID(parsInt(value))
case TotalPriceField:
c.TotalPrice = types.Price(parsInt(value))
case CreatedAtField:
c.CreatedAt = parsInt(value)
case ExpireAtField:
c.ExpireAt = parsInt(value)
}
}
return c, nil
}
func (r Repo) DeleteItem(ctx context.Context, userID, productID types.ID) error {
const op = "shoppingbasketapp.repository.DeleteItem"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(productID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
return err
}
if err := r.client.HDel(ctx, cartKey, itemKey).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
num, err := r.client.HLen(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if num < FieldNumber {
return r.DeleteCart(ctx, userID)
}
return r.updateTotalPrice(ctx, cartKey)
}
func (r Repo) UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error {
const op = "shoppingbasketapp.repository.UpdateQuantity"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(productID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
return err
}
data, err := r.client.HGet(ctx, cartKey, itemKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var item entity.Item
if err := json.Unmarshal([]byte(data), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
item.Quantity = quantity
j, _ := json.Marshal(item)
if err := r.client.HSet(ctx, cartKey, itemKey, string(j)).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return r.updateTotalPrice(ctx, cartKey)
}
func (r Repo) DeleteCart(ctx context.Context, userID types.ID) error {
const op = "shoppingbasketapp.repository.DeleteCart"
cartKey := r.cartKey(userID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.client.Del(ctx, cartKey).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}
func (r Repo) updateTotalPrice(ctx context.Context, cartKey string) error {
const op = "shoppingbasketapp.repository.updateTotalPrice"
allFields, err := r.client.HGetAll(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var total types.Price
for field, value := range allFields {
if strings.HasPrefix(field, "item:") {
var item entity.Item
if err := json.Unmarshal([]byte(value), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
total += item.Price * types.Price(item.Quantity)
}
}
return r.client.HSet(ctx, cartKey, TotalPriceField, int64(total)).Err()
}
func (r Repo) existsCart(ctx context.Context, cartKey string) error {
const op = "shoppingbasketapp.repository.existsCart"
exists, err := r.client.Exists(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if exists == 0 {
return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
}
return nil
}
func (r Repo) existsItem(ctx context.Context, cartKey, itemKey string) error {
const op = "shoppingbasketapp.repository.existsCart"
exists, err := r.client.HExists(ctx, cartKey, itemKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if !exists {
return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found product form shopping basket")
}
return nil
}

View File

@ -1,33 +0,0 @@
package service
import (
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type AddToCartRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
Quantity int `json:"quantity"`
Price types.Price `json:"price"`
Name string `json:"name"`
}
type GetCartResponse struct {
UserID types.ID `json:"user_id"`
Items []entity.Item `json:"items"`
TotalPrice types.Price `json:"total_price"`
CreatedAt int64 `json:"created_at"`
ExpireAt int64 `json:"expire_at"`
}
type RemoveFromCartRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
}
type UpdateQuantityRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
Quantity int `json:"quantity"`
}

View File

@ -1,104 +0,0 @@
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Repository interface {
AddItem(ctx context.Context, userID types.ID, item entity.Item) error
GetCart(ctx context.Context, userID types.ID) (entity.Cart, error)
DeleteItem(ctx context.Context, userID, productID types.ID) error
UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error
DeleteCart(ctx context.Context, userID types.ID) error
}
type Service struct {
validate Validate
repo Repository
}
func New(val Validate, repo Repository) Service {
return Service{validate: val, repo: repo}
}
func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error {
const op = "shoppingbasketapp.service.AddToBasket"
if err := s.validate.ValidateAddToCart(req); err != nil {
logger.L().Error("shoppingbasket-service-AddToBasket", "error", err)
return err
}
return s.repo.AddItem(ctx, req.UserID, entity.Item{
ProductID: req.ProductID,
Quantity: req.Quantity,
Price: req.Price,
Name: req.Name,
AddedAt: time.Now().UnixNano(),
})
}
func (s Service) GetCart(ctx context.Context, userID types.ID) (GetCartResponse, error) {
const op = "shoppingbasketapp.service.GetCart"
if userID < 1 {
logger.L().Error("shoppingbasket-service-GetCart", "error", "user id must be greater than 1")
return GetCartResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage("invalid user id")
}
res, err := s.repo.GetCart(ctx, userID)
if err != nil {
logger.L().Error("shoppingbasket-service-GetCart", "error", err)
return GetCartResponse{}, richerror.New(op).WithErr(err)
}
return GetCartResponse{
UserID: res.UserID,
Items: res.Items,
TotalPrice: res.TotalPrice,
CreatedAt: res.CreatedAt,
ExpireAt: res.ExpireAt,
}, nil
}
func (s Service) RemoveFromCart(ctx context.Context, req RemoveFromCartRequest) error {
const op = "shoppingbaskerapp.service.RemoveFromCart"
if err := s.validate.ValidateRemoveFromCart(req); err != nil {
logger.L().Error("shoppingbasket-service-RemoveFromCart", "error", err)
return err
}
return s.repo.DeleteItem(ctx, req.UserID, req.ProductID)
}
func (s Service) UpdateQuantity(ctx context.Context, req UpdateQuantityRequest) error {
const op = "shoppingbaskerapp.service.UpdateQuantity"
if err := s.validate.ValidateUpdateQuantity(req); err != nil {
logger.L().Error("shoppingbasket-service-UpdateQuantity", "error", err)
return err
}
if req.Quantity == 0 {
return s.repo.DeleteItem(ctx, req.UserID, req.ProductID)
}
return s.repo.UpdateQuantity(ctx, req.UserID, req.ProductID, req.Quantity)
}
func (s Service) ClearCart(ctx context.Context, userID types.ID) error {
const op = "shoppingbaskerapp.service.ClearCart"
if userID < 1 {
logger.L().Error("shoppingbasket-service-ClearCart", "error", "user id must be greater than 1")
return richerror.New(op).WithKind(richerror.KindInvalid).
WithMessage("invalid user id")
}
return s.repo.DeleteCart(ctx, userID)
}

View File

@ -1,91 +0,0 @@
package service
import (
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
const (
ErrValidationPositive = "must be positive"
ErrValidationInvalidInput = "invalid input"
)
type Validate struct{}
func NewValidate() Validate {
return Validate{}
}
func (v Validate) ValidateAddToCart(req AddToCartRequest) error {
const op = "shoppingbasketapp.service.AddToCart"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required),
validation.Field(&req.Price, validation.Required, validation.Min(int64(1)).Error(ErrValidationPositive)),
validation.Field(&req.Quantity, validation.Min(int(1)).Error(ErrValidationPositive)),
validation.Field(&req.Name, validation.Required)); err != nil {
fieldErr := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErr[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithMeta(fieldErr).WithErr(err).WithKind(richerror.KindInvalid)
}
return nil
}
func (v Validate) ValidateRemoveFromCart(req RemoveFromCartRequest) error {
const op = "shoppingbasketapp.service.ValidateRemoveFromCart"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required)); err != nil {
fieldErrs := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErrs[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithKind(richerror.KindInvalid).WithMeta(fieldErrs).WithErr(err)
}
return nil
}
func (v Validate) ValidateUpdateQuantity(req UpdateQuantityRequest) error {
const op = "shoppingbasketapp.service.ValidateUpdateQuantity"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required),
validation.Field(&req.Quantity, validation.Required, validation.Min(int(1)))); err != nil {
fieldErrs := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErrs[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithKind(richerror.KindInvalid).WithMeta(fieldErrs).WithErr(err)
}
return nil
}

View File

@ -1,28 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,21 +0,0 @@
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"`
}

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