forked from amir_tavakolian/niki
Compare commits
74 Commits
salesRepor
...
develop
| Author | SHA1 | Date |
|---|---|---|
|
|
3b22e99697 | |
|
|
0f49ce4156 | |
|
|
9f4e011357 | |
|
|
f3bf08e7b2 | |
|
|
d564883081 | |
|
|
2517737cc5 | |
|
|
95be95632c | |
|
|
11c73efd66 | |
|
|
6807b1c962 | |
|
|
6182f5781b | |
|
|
850bb3f3e0 | |
|
|
764c1714d6 | |
|
|
590ed0bb07 | |
|
|
a05c2fc7d5 | |
|
|
5fc6634c2b | |
|
|
5a05490502 | |
|
|
f021c559bb | |
|
|
631399313a | |
|
|
87923a3d51 | |
|
|
14e9a51fb6 | |
|
|
d1c2191c5c | |
|
|
c01d3678ae | |
|
|
4fcaef0e28 | |
|
|
0948b36012 | |
|
|
aae35bb787 | |
|
|
233df829fe | |
|
|
d4f65ba68a | |
|
|
9d97a4e313 | |
|
|
0269822fc0 | |
|
|
23fedc02c3 | |
|
|
f335b74488 | |
|
|
8b19bd8d8d | |
|
|
0556f3c345 | |
|
|
373ef9512f | |
|
|
b8a9460e69 | |
|
|
e6eae13d85 | |
|
|
49e0e7aa91 | |
|
|
d6396ef14f | |
|
|
a986f03e44 | |
|
|
447e62c693 | |
|
|
58c1f57de8 | |
|
|
d57adaebec | |
|
|
9ea2a5c493 | |
|
|
ade97cad9f | |
|
|
3a3351f358 | |
|
|
69df6c5781 | |
|
|
6c96b349ab | |
|
|
3369df246c | |
|
|
e7f7dfc6bd | |
|
|
f4756345e0 | |
|
|
588cf26ef4 | |
|
|
57faf27457 | |
|
|
df292897ed | |
|
|
39d85397d6 | |
|
|
7e202fffb9 | |
|
|
03cba27f4a | |
|
|
d12f77f14f | |
|
|
356a829c97 | |
|
|
f37c2a3128 | |
|
|
d06e6fe6c4 | |
|
|
00fb0a7ead | |
|
|
db13725994 | |
|
|
e72e05e64f | |
|
|
6511f76e65 | |
|
|
10dbee28bd | |
|
|
2037e58a1d | |
|
|
50d7c2a2dc | |
|
|
f4bd43a60f | |
|
|
8dd13ce6af | |
|
|
4ad9199a65 | |
|
|
c1ed70cf66 | |
|
|
3d5e4e473b | |
|
|
73411137c8 | |
|
|
6c48741646 |
|
|
@ -0,0 +1,34 @@
|
||||||
|
root = "."
|
||||||
|
tmp_dir = "cmd/productapp/temp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
bin = "/entrypoint.sh"
|
||||||
|
args_bin = []
|
||||||
|
cmd = "go build -mod=mod -buildvcs=false -o ./cmd/productapp/temp/main ./cmd/productapp/"
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["vendor", "cmd/productapp/temp"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = []
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go"]
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = true
|
||||||
|
poll = true
|
||||||
|
poll_interval = 1000
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
@ -30,3 +30,5 @@ logs/
|
||||||
mise.log
|
mise.log
|
||||||
|
|
||||||
curl
|
curl
|
||||||
|
|
||||||
|
cmd/**/temp/main
|
||||||
|
|
@ -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 migration files
|
# Copy migrations 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
|
||||||
|
|
|
||||||
124
Makefile
124
Makefile
|
|
@ -1,54 +1,64 @@
|
||||||
# TODO: add commands for build and run in dev/produciton mode
|
# --- Variables ---
|
||||||
|
BINARY_NAME ?= niki
|
||||||
|
BUILD_DIR ?= bin
|
||||||
|
|
||||||
ROOT=$(realpath $(dir $(lastword $(MAKEFILE_LIST))))
|
# ====================================================================================
|
||||||
|
# General Go Commands
|
||||||
|
# ====================================================================================
|
||||||
|
.PHONY: start test build clean mod-tidy lint install-linter help format swagger watch
|
||||||
|
|
||||||
.PHONY: help confirm lint test format build run docker swagger watch migrate/status migrate/new migrate/up migrate/down
|
start: build
|
||||||
|
$(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 $(ROOT)
|
@gofumpt -l -w .
|
||||||
@which gci || (go install github.com/daixiang0/gci@latest)
|
@which gci || (go install github.com/daixiang0/gci@latest)
|
||||||
@gci write $(ROOT) --skip-generated --skip-vendor
|
@gci write . --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="./niki"; \
|
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \
|
||||||
else \
|
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 \
|
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="./niki"; \
|
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -64,3 +74,67 @@ 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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package agentapp
|
|
||||||
|
|
||||||
type Application struct {
|
|
||||||
config Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func Setup() {}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package agentapp
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
// database config
|
|
||||||
// httpserver config
|
|
||||||
//...
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
// httpServer
|
|
||||||
// handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() Server {
|
|
||||||
return Server{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Server) Serve() {}
|
|
||||||
|
|
||||||
func (s Server) RegisterRoutes() {}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
||||||
type Agent struct {
|
|
||||||
ID uint
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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.`,
|
||||||
|
}
|
||||||
|
|
@ -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() {}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.gocasts.ir/ebhomengo/niki/cmd/payment/command"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
command.Execute()
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -25,27 +28,53 @@ var serveCmd = &cobra.Command{
|
||||||
func serve() {
|
func serve() {
|
||||||
log.Println("Product Service Starting...")
|
log.Println("Product Service Starting...")
|
||||||
|
|
||||||
// TODO: Initialize database connection
|
cfg := productapp.Config{
|
||||||
// TODO: Initialize service dependencies
|
HTTPServer: productapp.HTTPServerConfig{
|
||||||
// TODO: Setup HTTP server with routes
|
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() {
|
go func() {
|
||||||
sigCh := make(chan os.Signal, 1)
|
app.Start()
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sigCh
|
|
||||||
log.Println("Shutting down Product Service gracefully...")
|
|
||||||
os.Exit(0)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
quit := make(chan os.Signal, 1)
|
||||||
fmt.Fprintf(w, "Product Service OK!")
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
})
|
<-quit
|
||||||
|
|
||||||
log.Printf("Product Service listening on port %s", port)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
defer cancel()
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,7 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package command
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/cmd/shoppingbasketapp/command"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := command.RootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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/migration",
|
MigrationPath: "../../../repository/mysql/migrations",
|
||||||
MigrationDBName: "gorp_migrations",
|
MigrationDBName: "gorp_migrations",
|
||||||
})
|
})
|
||||||
migrations.Up()
|
migrations.Up()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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:
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
ARG GO_IMAGE_NAME
|
||||||
|
ARG GO_IMAGE_VERSION
|
||||||
|
|
||||||
|
FROM ${GO_IMAGE_NAME}:${GO_IMAGE_VERSION}
|
||||||
|
ENV GOPROXY=https://package-mirror.liara.ir/repository/go/
|
||||||
|
ENV GOSUMDB=off
|
||||||
|
|
||||||
|
WORKDIR /home/app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
RUN go install github.com/air-verse/air@latest
|
||||||
|
|
||||||
|
RUN printf '#!/bin/sh\n./cmd/productapp/temp/main migrate --up\nexec ./cmd/productapp/temp/main serve\n' > /entrypoint.sh && chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["air", "-c", "/home/app/.air/.air.productapp.toml"]
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
services:
|
||||||
|
productapp-mysql:
|
||||||
|
image: mirror2.chabokan.net/mysql:8.0
|
||||||
|
container_name: productapp-mysql
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3307:3306"
|
||||||
|
volumes:
|
||||||
|
- productapp-mysql-data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: niki_db
|
||||||
|
MYSQL_ROOT_PASSWORD: secret
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
productapp-mysql-data:
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
services:
|
||||||
|
productapp-app:
|
||||||
|
build:
|
||||||
|
context: ../../..
|
||||||
|
dockerfile: deploy/productapp/development/Dockerfile
|
||||||
|
args:
|
||||||
|
GO_IMAGE_NAME: ${GO_IMAGE_NAME}
|
||||||
|
GO_IMAGE_VERSION: ${GO_IMAGE_VERSION}
|
||||||
|
container_name: productapp-app
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ../../..:/home/app
|
||||||
|
environment:
|
||||||
|
DB_HOST: productapp-mysql
|
||||||
|
DB_USERNAME: root
|
||||||
|
DB_PASSWORD: secret
|
||||||
|
DB_NAME: niki_db
|
||||||
|
MIGRATION_PATH: /home/app/productapp/repository/migrations
|
||||||
|
depends_on:
|
||||||
|
productapp-mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
productapp-mysql:
|
||||||
|
image: mirror2.chabokan.net/mysql:8.0
|
||||||
|
container_name: productapp-mysql
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3307:3306"
|
||||||
|
volumes:
|
||||||
|
- productapp-mysql-data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: niki_db
|
||||||
|
MYSQL_ROOT_PASSWORD: secret
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
productapp-mysql-data:
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "git.gocasts.ir/ebhomengo/niki/pkg/types"
|
||||||
|
|
||||||
|
type Driver struct {
|
||||||
|
ID types.ID
|
||||||
|
PhoneNumber string
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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`;
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- +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`;
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ CREATE TABLE `campaigns` (
|
||||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
`title` VARCHAR(255) NOT NULL,
|
`title` VARCHAR(255) NOT NULL,
|
||||||
`description` TEXT,
|
`description` TEXT,
|
||||||
|
`link` VARCHAR(255) NULL,
|
||||||
|
`slogan` VARCHAR(255) NULL,
|
||||||
`goal_amount` DECIMAL(15,2) NOT NULL,
|
`goal_amount` DECIMAL(15,2) NOT NULL,
|
||||||
`raised_amount` DECIMAL(15,2) DEFAULT 0,
|
`raised_amount` DECIMAL(15,2) DEFAULT 0,
|
||||||
`status` VARCHAR(50) NOT NULL,
|
`status` VARCHAR(50) NOT NULL,
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
repo Repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() Service {
|
type Repo interface {
|
||||||
return Service{}
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "git.gocasts.ir/ebhomengo/niki/types"
|
||||||
|
|
||||||
|
type Shipping struct {
|
||||||
|
ID types.ID
|
||||||
|
Name string
|
||||||
|
Price types.Price
|
||||||
|
IsActive bool
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
|
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
|
||||||
CREATE TABLE `orders` (
|
CREATE TABLE `orders` (
|
||||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
`user_id` INT NOT NULL,
|
`user_id` INT,
|
||||||
`address` TEXT,
|
`address_id` INT,
|
||||||
`shipping_id` INT NOT NULL,
|
`shipping_id` INT NOT NULL,
|
||||||
`payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online',
|
`payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online',
|
||||||
`payment_status` ENUM('unpaid', 'paid', 'cancelled') DEFAULT 'unpaid',
|
`payment_status` ENUM('unpaid', 'paid') DEFAULT 'unpaid',
|
||||||
`process_status` ENUM('waiting-to-pay', 'processing', 'accepted', 'preparing', 'prepared', 'given-to-post', 'delivered', 'cancelled') DEFAULT 'waiting-to-pay',
|
`process_status` ENUM('waiting-to-pay', 'processing', 'accepted', 'preparing', 'prepared', 'given-to-post', 'delivered', 'cancelled') DEFAULT 'waiting-to-pay',
|
||||||
`total_amount` INT NOT NULL,
|
`total_amount` INT NOT NULL,
|
||||||
`total_discount` INT NULL,
|
`total_discount` INT NULL,
|
||||||
|
|
@ -15,7 +15,7 @@ CREATE TABLE `orders` (
|
||||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
-- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
|
-- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
|
||||||
-- FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`)
|
FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`)
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- +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`;
|
||||||
|
|
@ -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}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,14 @@
|
||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity"
|
||||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
|
|
||||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
|
||||||
"git.gocasts.ir/ebhomengo/niki/types"
|
"git.gocasts.ir/ebhomengo/niki/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DB struct {
|
|
||||||
conn *mysql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(db *mysql.DB) *DB {
|
|
||||||
return &DB{conn: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) {
|
func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) {
|
||||||
|
|
||||||
const Op = "repository.mysql.order.createorder"
|
const Op = "domain.repository.mysql.order.create-order"
|
||||||
tx, err := d.conn.Conn().Begin()
|
tx, err := d.conn.Conn().Begin()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -26,10 +17,10 @@ func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (typ
|
||||||
|
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
query := "insert into orders(user_id, address, shipping_id," +
|
query := "insert into orders(user_id, address_id, shipping_id," +
|
||||||
" payment_method, payment_status, process_status," +
|
" payment_method, payment_status, process_status," +
|
||||||
" total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);"
|
" total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);"
|
||||||
res, oErr := tx.Exec(query, order.UserID, order.Address, order.ShippingID,
|
res, oErr := tx.Exec(query, order.UserID, order.AddressID, order.ShippingID,
|
||||||
order.PaymentMethod, order.PaymentStatus, order.ProcessStatus,
|
order.PaymentMethod, order.PaymentStatus, order.ProcessStatus,
|
||||||
order.TotalAmount, order.TotalDiscount)
|
order.TotalAmount, order.TotalDiscount)
|
||||||
|
|
||||||
|
|
@ -57,7 +48,7 @@ func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (typ
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
|
func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
|
||||||
const Op = "repository.mysql.order.update-order-process-status"
|
const Op = "domain.repository.mysql.order.update-order-process-status"
|
||||||
_, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID)
|
_, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -67,3 +58,35 @@ func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, er
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetShipping() ([]entity.Shipping, error) {
|
||||||
|
const Op = "domain.repository.mysql.order.get-shipping"
|
||||||
|
rows, err := d.conn.Conn().Query("select * from shippings where is_active=1")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return []entity.Shipping{}, richerror.New(Op).WithErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var shippings []entity.Shipping
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var s entity.Shipping
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&s.ID,
|
||||||
|
&s.Name,
|
||||||
|
&s.Price,
|
||||||
|
&s.IsActive,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, richerror.New(Op).WithErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shippings = append(shippings, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shippings, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package order
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity"
|
||||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
|
types "git.gocasts.ir/ebhomengo/niki/types"
|
||||||
"git.gocasts.ir/ebhomengo/niki/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|
@ -13,26 +13,27 @@ type Service struct {
|
||||||
type Repo interface {
|
type Repo interface {
|
||||||
CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error)
|
CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error)
|
||||||
UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error)
|
UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error)
|
||||||
|
GetShipping() ([]entity.Shipping, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(orderRepo Repo) Service {
|
func New(orderRepo Repo) Service {
|
||||||
return Service{repo: orderRepo}
|
return Service{repo: orderRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (CreateOrderResponse, error) {
|
func (s *Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) {
|
||||||
const Op = "purchaseapp.service.CreateOrder"
|
const Op = "domain.order.service.order.CreateOrder"
|
||||||
orderID, err := s.repo.CreateOrder(order, orderItems)
|
orderID, err := s.repo.CreateOrder(order, orderItems)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CreateOrderResponse{}, richerror.New(Op).WithErr(err)
|
return 0, richerror.New(Op).WithErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreateOrderResponse{OrderID: orderID}, nil
|
return orderID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
|
func (s *Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
|
||||||
|
|
||||||
const Op = "purchaseapp.service.UpdateOrderProcessStatus"
|
const Op = "domain.order.service.order.UpdateOrderProcessStatus"
|
||||||
_, err := s.repo.UpdateOrderProcessStatus(orderID, status)
|
_, err := s.repo.UpdateOrderProcessStatus(orderID, status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, richerror.New(Op).WithErr(err)
|
return false, richerror.New(Op).WithErr(err)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type PaymentMethod struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
IsActive bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package gateway
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package database
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
type PaymentMethodService struct {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue