Compare commits

...

74 Commits

Author SHA1 Message Date
hossein 3b22e99697 Merge pull request 'add benefactor domain and implement its methods' (#300) from benefactor_domain into develop
Reviewed-on: ebhomengo/niki#300
2026-05-06 05:03:44 +00:00
hossein 0f49ce4156 Merge branch 'develop' into benefactor_domain 2026-05-06 05:00:23 +00:00
hossein 9f4e011357 Merge pull request 'campaign-domain' (#305) from campaign-domain into develop
Reviewed-on: ebhomengo/niki#305
2026-05-06 04:54:31 +00:00
hossein f3bf08e7b2 Merge branch 'develop' into campaign-domain 2026-05-06 04:52:35 +00:00
matina d564883081 contract protobuf for campaign servic 2026-05-06 02:38:22 -07:00
hossein 2517737cc5 Merge pull request 'refactor: remove data from cmd/order package to manage them later' (#301) from fix/order into develop
Reviewed-on: ebhomengo/niki#301
2026-05-06 04:48:59 +00:00
matina 95be95632c merge with dev 2026-05-05 11:48:18 -07:00
matina 11c73efd66 adding status interface 2026-05-05 11:12:03 -07:00
mokarramis 6807b1c962 refactor: remove data from cmd/order package to manage them later 2026-05-03 14:42:50 +03:30
unknown 6182f5781b add benefactor domain and implement its methods 2026-05-02 22:57:00 +03:30
hossein 850bb3f3e0 Merge pull request 'mehdikeshavarz/feat/accountDomain And driverDomain #285 #261' (#298) from mehdikeshavarz/driver(agent)/loginorregister into develop
Reviewed-on: ebhomengo/niki#298
2026-04-29 18:15:51 +00:00
fardin 764c1714d6 create account_app and move delivery layer to account_app 2026-04-29 19:58:57 -04:00
fardin 590ed0bb07 Merge branch 'develop' of https://git.gocasts.ir/ebhomengo/niki into mehdikeshavarz/driver(agent)/loginorregister 2026-04-29 09:24:30 -04:00
hossein a05c2fc7d5 Merge pull request 'resolved#289' (#293) from mahsaaghagolzadeh/issue#289-gRPC into develop
Reviewed-on: ebhomengo/niki#293
2026-04-29 05:10:47 +00:00
hossein 5fc6634c2b Merge branch 'develop' into mahsaaghagolzadeh/issue#289-gRPC 2026-04-29 05:07:23 +00:00
hossein 5a05490502 Merge pull request 'feature/product-app list api' (#296) from feature/product-app into develop
Reviewed-on: ebhomengo/niki#296
2026-04-29 05:06:52 +00:00
hossein f021c559bb Merge branch 'develop' into feature/product-app 2026-04-29 05:03:01 +00:00
hossein 631399313a Merge pull request 'init wallet service & postgres adapter #288' (#295) from feature/wallet into develop
Reviewed-on: ebhomengo/niki#295
2026-04-29 05:02:37 +00:00
hossein 87923a3d51 Merge branch 'develop' into feature/wallet 2026-04-29 05:00:06 +00:00
hossein 14e9a51fb6 Merge pull request 'feat(gamification): add gamification domain structure' (#290) from feature/gamification into develop
Reviewed-on: ebhomengo/niki#290
2026-04-29 04:55:28 +00:00
amir-ys d1c2191c5c feat: add product list api 2026-04-29 01:54:15 +03:30
amir-ys c01d3678ae feat: add product list api 2026-04-29 01:53:23 +03:30
fardin 4fcaef0e28 create account domain and impl loginorRegister for driver . 2026-04-28 18:09:44 -04:00
amir-ys 0948b36012 feat: add serve command with graceful shutdown support 2026-04-28 23:34:52 +03:30
danialasadi aae35bb787 init wallet service & postgres adapter 2026-04-28 21:07:05 +03:30
matina 233df829fe merge with develop 2026-04-28 01:49:48 -07:00
matina d4f65ba68a mapper for service & params & improve donation flow 2026-04-28 01:46:18 -07:00
amir-ys 9d97a4e313 feat: ignore cmd temp built files 2026-04-28 00:40:37 +03:30
amir-ys 0269822fc0 feat: add air for watch project changes 2026-04-28 00:38:31 +03:30
amir-ys 23fedc02c3 feat: add product domain entity 2026-04-28 00:05:25 +03:30
Sahar Mokarrami f335b74488 feat(gamification): add gamification domain structure 2026-04-24 14:36:10 +03:30
hossein 8b19bd8d8d Merge pull request 'resolved Issue #252' (#273) from mahsaaghagolzadeh/issue252-create-Payment into develop
Reviewed-on: ebhomengo/niki#273
2026-04-22 15:52:46 +00:00
hossein 0556f3c345 Merge branch 'develop' into mahsaaghagolzadeh/issue252-create-Payment 2026-04-22 15:48:20 +00:00
hossein 373ef9512f Merge pull request 'campaign' (#284) from campaign into develop
Reviewed-on: ebhomengo/niki#284
2026-04-22 15:48:06 +00:00
hossein b8a9460e69 Merge branch 'develop' into campaign 2026-04-22 15:43:59 +00:00
hossein e6eae13d85 Merge pull request 'refactor/shopping-basket-structure' (#281) from refactor/shopping-basket-structure into develop
Reviewed-on: ebhomengo/niki#281
2026-04-22 15:43:49 +00:00
hossein 49e0e7aa91 Merge branch 'develop' into refactor/shopping-basket-structure 2026-04-22 15:42:28 +00:00
hossein d6396ef14f Merge pull request 'fix: fix import package bug' (#280) from feature/order into develop
Reviewed-on: ebhomengo/niki#280
2026-04-22 15:42:05 +00:00
matina a986f03e44 delivery & handler 2026-04-21 00:04:58 -07:00
mahsaaghagolzadeh 447e62c693 mahsaaghagolzadeh/Issue#289 2026-04-20 23:26:15 +03:30
matina 58c1f57de8 merge with develop 2026-04-19 16:30:45 -07:00
matina d57adaebec improve structure of campaign & adding interfaces 2026-04-19 16:16:36 -07:00
fardin 9ea2a5c493 Merge branch 'develop' into mehdikeshavarz/driver(agent)/loginorregister 2026-04-19 18:55:37 -04:00
fardin ade97cad9f some change 2026-04-19 18:55:31 -04:00
fardin 3a3351f358 change config loader 2026-04-19 18:40:21 -04:00
mahsa-fox 69df6c5781 Merge branch 'develop' into mahsaaghagolzadeh/issue252-create-Payment 2026-04-19 05:01:01 +03:30
mahsa-fox 6c96b349ab Changed struct 2026-04-19 02:33:30 +03:30
mzfarshad 3369df246c bugfix imports path 2026-04-17 20:53:50 +03:30
Sahar Mokarrami e7f7dfc6bd fix: fix import package bug 2026-04-17 20:46:02 +03:30
mzfarshad f4756345e0 refactor shoppingbasketapp structure. service, repository in to domain directory 2026-04-17 19:36:23 +03:30
matina 588cf26ef4 feat & improve :(campaign domain) 2026-04-15 01:28:40 -07:00
matina 57faf27457 merge with develop 2026-04-15 01:15:44 -07:00
matina df292897ed refactor 2026-04-15 01:08:42 -07:00
hossein 39d85397d6 Merge pull request 'feat/shopping-basket' (#276) from feat/shopping-basket into develop
Reviewed-on: ebhomengo/niki#276
Reviewed-by: hossein <h.nazari1990@gmail.com>
2026-04-15 05:54:13 +00:00
hossein 7e202fffb9 Merge branch 'develop' into feat/shopping-basket 2026-04-15 05:52:43 +00:00
hossein 03cba27f4a Merge pull request 'donate' (#279) from donate into develop
Reviewed-on: ebhomengo/niki#279
2026-04-15 05:52:08 +00:00
hossein d12f77f14f Merge branch 'develop' into donate 2026-04-15 05:51:35 +00:00
hossein 356a829c97 Merge pull request 'feat(patient):Implement repository and docker-compose' (#275) from feature/repository-setup into develop
Reviewed-on: ebhomengo/niki#275
Reviewed-by: hossein <h.nazari1990@gmail.com>
2026-04-15 05:13:32 +00:00
hossein f37c2a3128 Merge branch 'develop' into feature/repository-setup 2026-04-15 05:12:20 +00:00
hossein d06e6fe6c4 Merge pull request 'feat(order): add order domain structure' (#272) from feature/order into develop
Reviewed-on: ebhomengo/niki#272
Reviewed-by: hossein <h.nazari1990@gmail.com>
2026-04-15 05:05:24 +00:00
fardin 00fb0a7ead feat(driver)impl logic send_otp and loginOrRegister 2026-04-14 19:35:45 -04:00
mzfarshad db13725994 refactor cmd layer 2026-04-14 19:19:12 +03:30
Mohammad Amin e72e05e64f Implement repository and docker-compose 2026-04-14 18:53:15 +03:30
mzfarshad 6511f76e65 Merge remote-tracking branch 'origin/develop' into feat/shopping-basket 2026-04-14 17:29:17 +03:30
mzfarshad 10dbee28bd changed log file path 2026-04-14 16:55:32 +03:30
mzfarshad 2037e58a1d added cmd and deploy layer 2026-04-14 16:29:03 +03:30
mzfarshad 50d7c2a2dc added app configuration and bootstrap setup 2026-04-13 23:51:03 +03:30
Sahar Mokarrami f4bd43a60f feat(order): add order domain structure 2026-04-13 10:32:58 +03:30
mzfarshad 8dd13ce6af added delivery layer 2026-04-12 23:34:52 +03:30
mzfarshad 4ad9199a65 implemented echo server instance in pkg directory 2026-04-12 23:34:27 +03:30
mzfarshad c1ed70cf66 added repository layer 2026-04-12 21:32:23 +03:30
mzfarshad 3d5e4e473b implemented service layer 2026-04-10 18:52:30 +03:30
mzfarshad 73411137c8 added shopping basket structure 2026-04-10 14:40:41 +03:30
mahsa-fox 6c48741646 Issue #252 2026-03-18 23:30:07 +03:30
935 changed files with 434559 additions and 19957 deletions

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

@ -0,0 +1,34 @@
root = "."
tmp_dir = "cmd/productapp/temp"
[build]
bin = "/entrypoint.sh"
args_bin = []
cmd = "go build -mod=mod -buildvcs=false -o ./cmd/productapp/temp/main ./cmd/productapp/"
delay = 1000
exclude_dir = ["vendor", "cmd/productapp/temp"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
include_dir = []
include_ext = ["go"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
poll = true
poll_interval = 1000
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false

2
.gitignore vendored
View File

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

View File

@ -19,7 +19,7 @@ FROM alpine:3.20 AS runtime
# Copy the binary from the builder stage # Copy the binary from the builder stage
COPY --from=builder /niki/niki . COPY --from=builder /niki/niki .
# Copy 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
View File

@ -1,54 +1,64 @@
# TODO: add commands for build and run in dev/produciton mode # --- Variables ---
BINARY_NAME ?= niki
BUILD_DIR ?= bin
ROOT=$(realpath $(dir $(lastword $(MAKEFILE_LIST)))) # ====================================================================================
# General Go Commands
# ====================================================================================
.PHONY: start test build clean mod-tidy lint install-linter help format swagger watch
.PHONY: help confirm lint test format build run docker swagger watch migrate/status migrate/new migrate/up migrate/down 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"

44
accountapp/app.go Normal file
View File

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

19
accountapp/config.go Normal file
View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -1 +0,0 @@
package http

View File

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

View File

@ -1 +0,0 @@
package repository

View File

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

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

7
cmd/payment/main.go Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
package command

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import (
func MigrateMariaDB(cfg mysql.Config) func() { 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()

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
ARG GO_IMAGE_NAME
ARG GO_IMAGE_VERSION
FROM ${GO_IMAGE_NAME}:${GO_IMAGE_VERSION}
ENV GOPROXY=https://package-mirror.liara.ir/repository/go/
ENV GOSUMDB=off
WORKDIR /home/app
COPY go.mod go.sum ./
RUN go mod download
RUN go install github.com/air-verse/air@latest
RUN printf '#!/bin/sh\n./cmd/productapp/temp/main migrate --up\nexec ./cmd/productapp/temp/main serve\n' > /entrypoint.sh && chmod +x /entrypoint.sh
COPY . .
CMD ["air", "-c", "/home/app/.air/.air.productapp.toml"]

View File

@ -0,0 +1,20 @@
services:
productapp-mysql:
image: mirror2.chabokan.net/mysql:8.0
container_name: productapp-mysql
restart: always
ports:
- "3307:3306"
volumes:
- productapp-mysql-data:/var/lib/mysql
environment:
MYSQL_DATABASE: niki_db
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
productapp-mysql-data:

View File

@ -0,0 +1,43 @@
services:
productapp-app:
build:
context: ../../..
dockerfile: deploy/productapp/development/Dockerfile
args:
GO_IMAGE_NAME: ${GO_IMAGE_NAME}
GO_IMAGE_VERSION: ${GO_IMAGE_VERSION}
container_name: productapp-app
ports:
- "8080:8080"
volumes:
- ../../..:/home/app
environment:
DB_HOST: productapp-mysql
DB_USERNAME: root
DB_PASSWORD: secret
DB_NAME: niki_db
MIGRATION_PATH: /home/app/productapp/repository/migrations
depends_on:
productapp-mysql:
condition: service_healthy
restart: unless-stopped
productapp-mysql:
image: mirror2.chabokan.net/mysql:8.0
container_name: productapp-mysql
restart: always
ports:
- "3307:3306"
volumes:
- productapp-mysql-data:/var/lib/mysql
environment:
MYSQL_DATABASE: niki_db
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
productapp-mysql-data:

View File

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

View File

View File

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
package gateway

View File

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

View File

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

View File

@ -0,0 +1 @@
package database

View File

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

View File

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

View File

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

View File

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

View File

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