diff --git a/benefactorapp/app.go b/benefactorapp/app.go
index 3a01a26f..aa4beab4 100644
--- a/benefactorapp/app.go
+++ b/benefactorapp/app.go
@@ -1,8 +1,62 @@
package benefactorapp
-import "net/http"
+import (
+ "context"
+ benefactorHttp "git.gocasts.ir/ebhomengo/niki/benefactorapp/delivery/http"
+ repo "git.gocasts.ir/ebhomengo/niki/benefactorapp/repository/database"
+ benefactor "git.gocasts.ir/ebhomengo/niki/benefactorapp/service"
+ mySql "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
+ httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
+ logger "git.gocasts.ir/ebhomengo/niki/pkg/logger"
+)
type Application struct {
- Config Config
- HTTPServer *http.Server
+ Config Config
+ HTTPServer benefactorHttp.Server
+ BenefactorService benefactor.Service
+ BenefactorHandler benefactorHttp.Handler
+ BenefactorRepo benefactor.Repository
+ DBConn mySql.DB
+}
+
+func Setup(ctx context.Context, config Config, DB mySql.DB) *Application {
+ log := logger.L()
+ log.Info("logger starting ...")
+
+ db := mySql.New(config.MySQLDB)
+ defer func() {
+ if err := db.CloseStatements(); err != nil {
+ log.Info("Error closing statements: %v\n", err)
+ }
+ }()
+ log.Info("mysql connection starting ...")
+
+ // Initialize repositories
+ benefactorRepo := repo.New(db)
+ benefactorValidator := benefactor.NewValidator(benefactorRepo)
+
+ benefactorSvc := benefactor.NewService(benefactorRepo, benefactorValidator)
+ benefactorHandler := benefactorHttp.NewHandler(benefactorSvc)
+
+ hServer, hErr := httpserver.New(config.HTTPServer)
+ if hErr != nil {
+ log.Error("Http Server error: %v,\n", hErr)
+ }
+ httpServer := benefactorHttp.NewServer(*hServer, *benefactorHandler)
+
+ return &Application{
+ Config: config,
+ HTTPServer: httpServer,
+ BenefactorService: benefactorSvc,
+ BenefactorHandler: *benefactorHandler,
+ BenefactorRepo: benefactorRepo,
+ DBConn: DB,
+ }
+
+}
+
+func (app *Application) Start() {
+ log := logger.L()
+ log.Info("app starting ...")
+ // TODO implementaion
}
diff --git a/benefactorapp/config.go b/benefactorapp/config.go
index de3d3f88..f399aaba 100644
--- a/benefactorapp/config.go
+++ b/benefactorapp/config.go
@@ -1,11 +1,23 @@
package benefactorapp
+import (
+ database "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
+ httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
+ "git.gocasts.ir/ebhomengo/niki/pkg/logger"
+)
+
type Config struct {
// HTTP server config
+ HTTPServer httpserver.Config `koanf:"http_server"`
// Database config
+ MySQLDB database.Config `koanf:"mariadb"`
// Logger config
+ Logger logger.Config `koanf:"logger"`
// Service config
+
+ // Database migration
+ PathOfMigration string `koanf:"path_of_migration"`
}
diff --git a/benefactorapp/delivery/http/handler.go b/benefactorapp/delivery/http/handler.go
index e9549c3c..c3966ddb 100644
--- a/benefactorapp/delivery/http/handler.go
+++ b/benefactorapp/delivery/http/handler.go
@@ -1,15 +1,20 @@
package http
import (
+ benefactor "git.gocasts.ir/ebhomengo/niki/benefactorapp/service"
"net/http"
"github.com/labstack/echo/v4"
)
-type Handler struct{}
+type Handler struct {
+ BebefactorService benefactor.Service
+}
-func NewHandler() *Handler {
- return &Handler{}
+func NewHandler(bService benefactor.Service) *Handler {
+ return &Handler{
+ BebefactorService: bService,
+ }
}
func (h Handler) HealthCheck(c echo.Context) error {
diff --git a/benefactorapp/delivery/http/server.go b/benefactorapp/delivery/http/server.go
index c5adfb44..4bdf3e65 100644
--- a/benefactorapp/delivery/http/server.go
+++ b/benefactorapp/delivery/http/server.go
@@ -1,16 +1,16 @@
package http
-import httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
+import httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
type Server struct {
- HTTPServer *httpserver.Server
- Handler *Handler
+ HTTPServer httpserver.Server
+ Handler Handler
}
-func NewServer(httpserver *httpserver.Server) *Server {
- return &Server{
+func NewServer(httpserver httpserver.Server, handler Handler) Server {
+ return Server{
HTTPServer: httpserver,
- Handler: NewHandler(),
+ Handler: handler,
}
}
diff --git a/benefactorapp/repository/database/db.go b/benefactorapp/repository/database/db.go
index 5d07c611..e3fef1fb 100644
--- a/benefactorapp/repository/database/db.go
+++ b/benefactorapp/repository/database/db.go
@@ -1,6 +1,6 @@
package database
-import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
+import "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
type DB struct {
conn *mysql.DB
diff --git a/benefactorapp/repository/database/migration/1775190076-create-benefacotors-table.sql b/benefactorapp/repository/database/migration/1775190076-create-benefacotors-table.sql
new file mode 100644
index 00000000..1a9b03ed
--- /dev/null
+++ b/benefactorapp/repository/database/migration/1775190076-create-benefacotors-table.sql
@@ -0,0 +1,20 @@
+-- +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 `benefactors1` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `first_name` VARCHAR(191),
+ `last_name` VARCHAR(191),
+ `phone_number` VARCHAR(191) NOT NULL UNIQUE,
+ `description` TEXT,
+ `email` VARCHAR(191),
+ `gender` ENUM('male','female'),
+ `birth_date` TIMESTAMP,
+ `status` ENUM('active','inactive') NOT NULL DEFAULT 'active',
+
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+-- +migrate Down
+DROP TABLE `benefactors1`;
\ No newline at end of file
diff --git a/benefactorapp/repository/database/migration/1775190077-create-addresses-table.sql b/benefactorapp/repository/database/migration/1775190077-create-addresses-table.sql
new file mode 100644
index 00000000..f5398634
--- /dev/null
+++ b/benefactorapp/repository/database/migration/1775190077-create-addresses-table.sql
@@ -0,0 +1,22 @@
+-- +migrate Up
+CREATE TABLE `addresses1` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `postal_code` VARCHAR(191) NOT NULL,
+ `address` TEXT NOT NULL,
+ `lat` FLOAT,
+ `lon` FLOAT,
+ `name` VARCHAR(191) NOT NULL,
+ `city_id` INT NOT NULL,
+ `province_id` INT NOT NULL,
+ `benefactor_id` INT NOT NULL,
+
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `deleted_at` TIMESTAMP,
+ FOREIGN KEY (`province_id`) REFERENCES `provinces` (`id`),
+ FOREIGN KEY (`city_id`) REFERENCES `cities` (`id`),
+ FOREIGN KEY (`benefactor_id`) REFERENCES `benefactors1` (`id`)
+);
+
+-- +migrate Down
+DROP TABLE `addresses1`;
\ No newline at end of file
diff --git a/benefactorapp/service/service.go b/benefactorapp/service/service.go
index 6d43c336..5cd2167b 100644
--- a/benefactorapp/service/service.go
+++ b/benefactorapp/service/service.go
@@ -1 +1,17 @@
package service
+
+type Service struct {
+ repository Repository
+ validator Validator
+}
+
+type Repository interface {
+ //GetList(ctx context.Context, ID types.ID) ([]entity.Benefactor, error)
+}
+
+func NewService(repo Repository, validator Validator) Service {
+ return Service{
+ repository: repo,
+ validator: validator,
+ }
+}
diff --git a/benefactorapp/service/validator.go b/benefactorapp/service/validator.go
index 6d43c336..21bc13dd 100644
--- a/benefactorapp/service/validator.go
+++ b/benefactorapp/service/validator.go
@@ -1 +1,11 @@
package service
+
+type ValidatorBenefactorRepository interface {
+}
+type Validator struct {
+ repo ValidatorBenefactorRepository
+}
+
+func NewValidator(repo ValidatorBenefactorRepository) Validator {
+ return Validator{repo: repo}
+}
diff --git a/cmd/benefactor/command/migrate.go b/cmd/benefactor/command/migrate.go
new file mode 100644
index 00000000..46257b78
--- /dev/null
+++ b/cmd/benefactor/command/migrate.go
@@ -0,0 +1,53 @@
+package command
+
+import (
+ migrator "git.gocasts.ir/ebhomengo/niki/pkg/database/migrator"
+ "git.gocasts.ir/ebhomengo/niki/pkg/logger"
+ "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 benefactor service.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ migrate()
+ },
+}
+
+func migrate() {
+ var cfg = loadAppConfig()
+ mysqlConfig := getMysqlConfig()
+
+ logger.Init(cfg.Logger)
+ log := logger.L()
+
+ migratorCfg := migrator.Config{
+ MysqlConfig: mysqlConfig,
+ MigrationPath: cfg.PathOfMigration,
+ MigrationDBName: "gorp_migrations",
+ }
+ // Run migrations if flags are set
+ if migrateUp || migrateDown {
+ mgr := migrator.New(migratorCfg)
+ if migrateUp {
+ log.Info("Running migrations up...")
+ mgr.Up()
+ log.Info("Migrations up completed.")
+ }
+ if migrateDown {
+ log.Info("Running migrations down...")
+ mgr.Down()
+ log.Info("Migrations down completed.")
+ }
+ }
+}
+
+func init() {
+ migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up")
+ migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down")
+ RootCmd.AddCommand(migrateCmd)
+}
diff --git a/cmd/benefactor/command/root.go b/cmd/benefactor/command/root.go
new file mode 100644
index 00000000..5b3539b3
--- /dev/null
+++ b/cmd/benefactor/command/root.go
@@ -0,0 +1,80 @@
+package command
+
+import (
+ "fmt"
+ "git.gocasts.ir/ebhomengo/niki/benefactorapp"
+ cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader"
+ "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
+ database "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
+ serviceConfigMysql "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
+ "git.gocasts.ir/ebhomengo/niki/pkg/path"
+ "github.com/spf13/cobra"
+ "log"
+ "os"
+ "path/filepath"
+)
+
+var RootCmd = &cobra.Command{
+ Use: "benefactor_service",
+ Short: "A CLI for benefactor Service",
+ Long: `benefactor Service CLI is a tool to manage and run
+the benefactor service, including migrations and server startup.`,
+}
+
+func getMysqlConfig() mysql.Config {
+ var cfg = loadAppConfig()
+
+ mysqlConfig := serviceConfigMysql.Config{
+ Username: cfg.MySQLDB.Username,
+ Password: cfg.MySQLDB.Password,
+ Port: cfg.MySQLDB.Port,
+ Host: cfg.MySQLDB.Host,
+ DBName: cfg.MySQLDB.DBName,
+ }
+
+ return mysqlConfig
+}
+
+func getDB(cfg database.Config) *mysql.DB {
+
+ db := serviceConfigMysql.New(cfg)
+ defer func() {
+ if err := db.CloseStatements(); err != nil {
+ fmt.Printf("Error closing statements: %v\n", err)
+ }
+ }()
+ return db
+}
+func loadAppConfig() benefactorapp.Config {
+ var cfg benefactorapp.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", "benefactor", "development", "config.yml")
+ if _, err := os.Stat(defaultConfig); err == nil {
+ yamlPath = defaultConfig
+ } else {
+ yamlPath = filepath.Join(projectRoot, "deploy", "benefactor", "development", "config.local.yml")
+ }
+ }
+
+ options := cfgloader.Option{
+ Prefix: "BENEFACTOR_",
+ 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
+}
diff --git a/cmd/benefactor/command/serve.go b/cmd/benefactor/command/serve.go
new file mode 100644
index 00000000..ca141307
--- /dev/null
+++ b/cmd/benefactor/command/serve.go
@@ -0,0 +1,47 @@
+package command
+
+import (
+ "context"
+ "git.gocasts.ir/ebhomengo/niki/benefactorapp"
+ "git.gocasts.ir/ebhomengo/niki/pkg/logger"
+ "github.com/spf13/cobra"
+)
+
+var migrateUp bool
+var migrateDown bool
+
+var serveCmd = &cobra.Command{
+ Use: "serve",
+ Short: "Start the benefactor service",
+ Long: `This command starts the main benefactor service.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ serve()
+ },
+}
+
+func serve() {
+ var cfg = loadAppConfig()
+
+ // Initialize logger
+ logger.Init(cfg.Logger)
+ log := logger.L()
+
+ db := getDB(cfg.MySQLDB)
+ migrate()
+
+ // Start the server
+ log.Info("Starting benefactor Service...")
+
+ // Connect to the database
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ app := benefactorapp.Setup(ctx, cfg, *db)
+ app.Start()
+}
+
+func init() {
+ serveCmd.Flags().BoolVar(&migrateUp, "migrate-up", false, "Run migrations up before starting the server")
+ serveCmd.Flags().BoolVar(&migrateDown, "migrate-down", false, "Run migrations down before starting the server")
+ RootCmd.AddCommand(serveCmd)
+}
diff --git a/cmd/benefactor/main.go b/cmd/benefactor/main.go
new file mode 100644
index 00000000..e74e381f
--- /dev/null
+++ b/cmd/benefactor/main.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+ "git.gocasts.ir/ebhomengo/niki/cmd/benefactor/command"
+ "os"
+)
+
+func main() {
+ if err := command.RootCmd.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
diff --git a/cmd/productapp/command/migrate.go b/cmd/productapp/command/migrate.go
new file mode 100644
index 00000000..feeb43e4
--- /dev/null
+++ b/cmd/productapp/command/migrate.go
@@ -0,0 +1,74 @@
+package command
+
+import (
+ "log"
+ "os"
+ "path/filepath"
+
+ "git.gocasts.ir/ebhomengo/niki/repository/migrator"
+ "git.gocasts.ir/ebhomengo/niki/repository/mysql"
+ "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 product service.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ migrate()
+ },
+}
+
+func migrate() {
+ workingDir, err := os.Getwd()
+ if err != nil {
+ log.Fatalf("Error getting working directory: %v", err)
+ }
+
+ migrationPath := filepath.Join(workingDir, "productapp", "repository", "migrations")
+
+ // to run migrations when you want to run product service locally
+ if path := os.Getenv("MIGRATION_PATH"); path != "" {
+ migrationPath = path
+ log.Printf("Using override migration path: %s", migrationPath)
+ } else {
+ log.Printf("Using default migration path: %s", migrationPath)
+ }
+
+ // TODO: Load config from environment or config file
+ mgr := migrator.New(migrator.Config{
+ MysqlConfig: mysql.Config{
+ Username: getEnv("DB_USERNAME", "root"),
+ Password: getEnv("DB_PASSWORD", ""),
+ Port: 3306,
+ Host: getEnv("DB_HOST", "localhost"),
+ DBName: getEnv("DB_NAME", "niki_db"),
+ },
+ MigrationPath: migrationPath,
+ MigrationDBName: "product_migrations",
+ })
+
+ if up {
+ mgr.Up()
+ } else if down {
+ mgr.Down()
+ } else {
+ log.Println("Please specify a migration direction with --up or --down")
+ }
+}
+
+func getEnv(key, defaultValue string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+func init() {
+ migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up")
+ migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down")
+ RootCmd.AddCommand(migrateCmd)
+}
diff --git a/cmd/productapp/command/root.go b/cmd/productapp/command/root.go
new file mode 100644
index 00000000..f23379f5
--- /dev/null
+++ b/cmd/productapp/command/root.go
@@ -0,0 +1,10 @@
+package command
+
+import "github.com/spf13/cobra"
+
+var RootCmd = &cobra.Command{
+ Use: "product_service",
+ Short: "A CLI for Product Service",
+ Long: `Product Service CLI is a tool to manage and run
+the product service, including migrations and server startup.`,
+}
diff --git a/cmd/productapp/command/serve.go b/cmd/productapp/command/serve.go
new file mode 100644
index 00000000..6f60c82f
--- /dev/null
+++ b/cmd/productapp/command/serve.go
@@ -0,0 +1,54 @@
+package command
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/spf13/cobra"
+)
+
+var port string
+
+var serveCmd = &cobra.Command{
+ Use: "serve",
+ Short: "Start the product service",
+ Long: `This command starts the main product service.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ serve()
+ },
+}
+
+func serve() {
+ log.Println("Product Service Starting...")
+
+ // TODO: Initialize database connection
+ // TODO: Initialize service dependencies
+ // TODO: Setup HTTP server with routes
+
+ // Setup graceful shutdown
+ go func() {
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+ <-sigCh
+ log.Println("Shutting down Product Service gracefully...")
+ os.Exit(0)
+ }()
+
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Product Service OK!")
+ })
+
+ log.Printf("Product Service listening on port %s", port)
+ if err := http.ListenAndServe(":"+port, nil); err != nil {
+ log.Fatalf("Failed to start server: %v", err)
+ }
+}
+
+func init() {
+ serveCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on")
+ RootCmd.AddCommand(serveCmd)
+}
diff --git a/cmd/productapp/main.go b/cmd/productapp/main.go
new file mode 100644
index 00000000..e1bca56e
--- /dev/null
+++ b/cmd/productapp/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+
+ "git.gocasts.ir/ebhomengo/niki/cmd/productapp/command"
+)
+
+func main() {
+ if err := command.RootCmd.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
diff --git a/cmd/purchaseapp/command/migrate.go b/cmd/purchaseapp/command/migrate.go
new file mode 100644
index 00000000..d47dcf0d
--- /dev/null
+++ b/cmd/purchaseapp/command/migrate.go
@@ -0,0 +1 @@
+package command
diff --git a/cmd/purchaseapp/command/root.go b/cmd/purchaseapp/command/root.go
new file mode 100644
index 00000000..d47dcf0d
--- /dev/null
+++ b/cmd/purchaseapp/command/root.go
@@ -0,0 +1 @@
+package command
diff --git a/cmd/purchaseapp/command/serve.go b/cmd/purchaseapp/command/serve.go
new file mode 100644
index 00000000..d47dcf0d
--- /dev/null
+++ b/cmd/purchaseapp/command/serve.go
@@ -0,0 +1 @@
+package command
diff --git a/cmd/purchaseapp/main.go b/cmd/purchaseapp/main.go
new file mode 100644
index 00000000..fdd06b0b
--- /dev/null
+++ b/cmd/purchaseapp/main.go
@@ -0,0 +1,63 @@
+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() {
+ 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)
+}
diff --git a/cmd/staffapp/main.go b/cmd/staffapp/main.go
index 0d684245..6a3e15d1 100644
--- a/cmd/staffapp/main.go
+++ b/cmd/staffapp/main.go
@@ -1,15 +1,143 @@
package main
import (
+ "encoding/json"
"fmt"
"log"
"net/http"
+ "strconv"
+
+ "git.gocasts.ir/ebhomengo/niki/staffapp/repository/database"
+ "git.gocasts.ir/ebhomengo/niki/staffapp/service"
)
func main() {
- fmt.Println(" Staffapp Server Starting...")
+
+ staffDb := database.New()
+ staffService := service.NewStaffService(staffDb)
+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Staffapp OK!")
})
+
+ http.HandleFunc("/staff", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var newStaff service.Staff
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&newStaff)
+ if err != nil {
+ http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ createdStaff, err := staffService.RegisterStaff(newStaff.Name, newStaff.LastName, newStaff.PhoneNumber)
+ if err != nil {
+ http.Error(w, "Failed to register staff: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(createdStaff)
+ })
+
+ http.HandleFunc("/staff/", func(w http.ResponseWriter, r *http.Request) {
+
+ idStr := r.URL.Path[len("/staff/"):]
+ if idStr == "" {
+ http.Error(w, "Missing staff ID", http.StatusBadRequest)
+ return
+ }
+
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ http.Error(w, "Invalid staff ID format", http.StatusBadRequest)
+ return
+ }
+
+ switch r.Method {
+ case http.MethodGet:
+
+ st, err := staffService.Get(id)
+ if err != nil {
+
+ if err.Error() == "staff not found" {
+ http.Error(w, "Staff not found", http.StatusNotFound)
+ } else {
+ http.Error(w, "Failed to fetch staff: "+err.Error(), http.StatusInternalServerError)
+ }
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(st)
+
+ case http.MethodPut:
+
+ var staffData struct {
+ Name string `json:"Name"`
+ LastName string `json:"LastName"`
+ PhoneNumber string `json:"PhoneNumber"`
+ }
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&staffData); err != nil {
+ http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ updatedStaff, err := staffService.Update(id, staffData.Name, staffData.LastName, staffData.PhoneNumber)
+ if err != nil {
+
+ if err.Error() == "staff not found" {
+ http.Error(w, "Staff not found", http.StatusNotFound)
+ } else {
+ http.Error(w, "Failed to update staff: "+err.Error(), http.StatusInternalServerError)
+ }
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(updatedStaff)
+
+ case http.MethodDelete:
+
+ err = staffService.Remove(id)
+ if err != nil {
+ if err.Error() == "staff not found" {
+ http.Error(w, "Staff not found", http.StatusNotFound)
+ } else {
+ http.Error(w, "Failed to remove staff: "+err.Error(), http.StatusInternalServerError)
+ }
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+
+ default:
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ }
+ })
+
+ http.HandleFunc("/staffs", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ list, err := staffService.List()
+ if err != nil {
+ http.Error(w, "Failed to fetch staff list: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(list)
+ })
+
log.Fatal(http.ListenAndServe(":8080", nil))
}
diff --git a/config/loader.go b/config/loader.go
index 3867c153..58de5ef9 100644
--- a/config/loader.go
+++ b/config/loader.go
@@ -12,12 +12,13 @@ import (
)
const (
- defaultPrefix = "EB_"
- defaultDelimiter = "."
- defaultSeparator = "__"
- defaultYamlFilePath = "config.yml"
+ defaultPrefix = "EB_"
+ defaultDelimiter = "."
+ defaultSeparator = "__"
)
+var defaultYamlFilePath = "config.yml"
+
var c Config
type Option struct {
diff --git a/delivery/http_server/end2end/setup/docker.go b/delivery/http_server/end2end/setup/docker.go
index 2eddecb0..fd3c718e 100644
--- a/delivery/http_server/end2end/setup/docker.go
+++ b/delivery/http_server/end2end/setup/docker.go
@@ -18,8 +18,8 @@ type TestContainer struct {
dockerPool *dockertest.Pool // the connection pool to Docker.
mariaResource *dockertest.Resource // MariaDB Docker container resource.
redisResource *dockertest.Resource // Redis Docker container resource.
- mariaDBConn *mysql.DB // Connection to the MariaDB database.
- redisDBConn *redisadapter.Adapter // Connection to the Redis database.
+ mariaDBConn *mysql.DB // Connection to the MariaDB mysql.
+ redisDBConn *redisadapter.Adapter // Connection to the Redis mysql.
containerExpiryInSeconds uint
}
@@ -158,7 +158,7 @@ func (t *TestContainer) Start() {
return nil
}); err != nil {
- log.Fatalf("Could not connect to database: %s", err)
+ log.Fatalf("Could not connect to mysql: %s", err)
}
}
diff --git a/deploy/benefactor/development/Dockerfile b/deploy/benefactor/development/Dockerfile
index e69de29b..3d8f9272 100644
--- a/deploy/benefactor/development/Dockerfile
+++ b/deploy/benefactor/development/Dockerfile
@@ -0,0 +1,2 @@
+FROM golang:1.25-alpine
+
diff --git a/deploy/benefactor/development/config.yml b/deploy/benefactor/development/config.yml
index e69de29b..084cbebd 100644
--- a/deploy/benefactor/development/config.yml
+++ b/deploy/benefactor/development/config.yml
@@ -0,0 +1,30 @@
+http_server:
+ host: ""
+ port: 1308
+ shutdown_context_timeout: 10s
+ cors:
+ allow_origins:
+ - "*"
+
+logger:
+ level: "debug" # Can be `debug`, `info`, `warn`, `error`
+ file_path: "logs/benefactorapp/service.log"
+ use_local_time: true
+ file_max_size_in_mb: 10
+ file_max_age_in_days: 7
+
+database_retry:
+ max_retries: 3
+ retry_delay: 100ms
+
+total_shutdown_timeout: 30m
+
+path_of_migration: "./benefactorapp/repository/migration"
+
+mariadb:
+ port: 3306
+ host: localhost
+ db_name: niki_db
+ username: niki
+ password: nikiappt0lk2o20
+
diff --git a/domain/notification/entity/channel.go b/domain/notification/entity/channel.go
new file mode 100644
index 00000000..4dfcc563
--- /dev/null
+++ b/domain/notification/entity/channel.go
@@ -0,0 +1,8 @@
+package entity
+
+type Channel struct {
+ ID int8
+ Type NotificationType
+ Provider string
+ Config string
+}
diff --git a/domain/notification/entity/notification.go b/domain/notification/entity/notification.go
new file mode 100644
index 00000000..96887c62
--- /dev/null
+++ b/domain/notification/entity/notification.go
@@ -0,0 +1,51 @@
+package entity
+
+type Notification struct {
+ ID int8
+ Type NotificationType
+ Recipinet string
+ Body string
+ Status NotificationStatus
+}
+
+type NotificationType uint8
+
+const (
+ Email NotificationType = iota + 1
+ SMS
+ Push
+)
+
+func (t NotificationType) String() string {
+ switch t {
+ case Email:
+ return "Email"
+ case SMS:
+ return "SMS"
+ case Push:
+ return "Push"
+ default:
+ return "Unknown"
+ }
+}
+
+type NotificationStatus uint8
+
+const (
+ Pending NotificationStatus = iota + 1
+ Success
+ Failed
+)
+
+func (t NotificationStatus) String() string {
+ switch t {
+ case Pending:
+ return "Pending"
+ case Success:
+ return "Success"
+ case Failed:
+ return "Failed"
+ default:
+ return "Unknown"
+ }
+}
diff --git a/domain/notification/messagebroker/redis.go b/domain/notification/messagebroker/redis.go
new file mode 100644
index 00000000..c6974a9e
--- /dev/null
+++ b/domain/notification/messagebroker/redis.go
@@ -0,0 +1,17 @@
+package messagebroker
+
+import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
+
+type redis struct {
+}
+
+func (r *redis) AddItem(notification entity.Notification) error {
+ return nil
+}
+
+func (r *redis) RemoveItem(notification entity.Notification) error {
+ return nil
+}
+func (r *redis) HealthCheck() error {
+ return nil
+}
diff --git a/domain/notification/service/additem.go b/domain/notification/service/additem.go
new file mode 100644
index 00000000..d79a9677
--- /dev/null
+++ b/domain/notification/service/additem.go
@@ -0,0 +1,67 @@
+package service
+
+import (
+ _ "time"
+
+ "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
+)
+
+type Notifservice struct {
+ MessageBroker MessageBroker
+ Repository Repository
+ Channel Channel
+}
+
+type MessageBroker interface {
+ AddItem(notification entity.Notification) error
+ RemoveItem(notification entity.Notification) error
+ HealthCheck() error
+}
+
+type Repository interface {
+ AddItem(notification entity.Notification) error
+}
+
+type Channel interface {
+ SendMessage(notification entity.Notification) error
+}
+type NotificationServiceRequest struct {
+ Body string
+ Type entity.NotificationType
+ Recipinet string
+}
+
+func (n *Notifservice) NewNotification(r NotificationServiceRequest) *entity.Notification {
+ // TODO add validation of notification properties
+ return &entity.Notification{
+ Type: r.Type,
+ Recipinet: r.Recipinet,
+ Body: r.Body,
+ Status: entity.Pending,
+ }
+}
+
+func (n *Notifservice) Send(notification entity.Notification) error {
+ if err := n.Channel.SendMessage(notification); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (n *Notifservice) Add(notification *entity.Notification) error {
+ err := n.MessageBroker.AddItem(*notification)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (n *Notifservice) Archive(notification *entity.Notification) error {
+ if err := n.MessageBroker.RemoveItem(*notification); err != nil {
+ return err
+ }
+ if err := n.Repository.AddItem(*notification); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/domain/notification/service/send.go b/domain/notification/service/send.go
new file mode 100644
index 00000000..1d2c9211
--- /dev/null
+++ b/domain/notification/service/send.go
@@ -0,0 +1,7 @@
+package service
+
+import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
+
+type sender interface {
+ Send(notification entity.Notification) error
+}
diff --git a/go.mod b/go.mod
index 42258236..f86c60a6 100644
--- a/go.mod
+++ b/go.mod
@@ -7,23 +7,26 @@ require (
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/go-sql-driver/mysql v1.9.3
+ github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
github.com/knadh/koanf v1.5.0
+ github.com/knadh/koanf/v2 v2.3.0
github.com/labstack/echo-jwt/v4 v4.4.0
github.com/labstack/echo/v4 v4.15.1
github.com/ory/dockertest/v3 v3.12.0
github.com/redis/go-redis/v9 v9.18.0
github.com/rubenv/sql-migrate v1.8.1
+ github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/swaggo/echo-swagger v1.5.2
github.com/swaggo/swag v1.16.6
- golang.org/x/crypto v0.46.0
+ golang.org/x/crypto v0.48.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
- dario.cat/mergo v1.0.0 // indirect
+ dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
@@ -36,20 +39,21 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v27.4.1+incompatible // indirect
- github.com/docker/docker v27.1.1+incompatible // indirect
- github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/docker v28.3.3+incompatible // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
- github.com/fsnotify/fsnotify v1.4.9 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
- github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -59,14 +63,15 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
- github.com/moby/sys/user v0.3.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
- github.com/opencontainers/image-spec v1.1.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/sv-tools/openapi v0.2.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
@@ -77,13 +82,13 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
- golang.org/x/mod v0.30.0 // indirect
- golang.org/x/net v0.48.0 // indirect
- golang.org/x/sync v0.19.0 // indirect
- golang.org/x/sys v0.39.0 // indirect
- golang.org/x/text v0.32.0 // indirect
+ golang.org/x/mod v0.33.0 // indirect
+ golang.org/x/net v0.50.0 // indirect
+ golang.org/x/sync v0.20.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
- golang.org/x/tools v0.39.0 // indirect
+ golang.org/x/tools v0.42.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
diff --git a/go.sum b/go.sum
index 940950dd..eb38c785 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
-dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
@@ -58,6 +58,7 @@ github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -68,10 +69,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
-github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
-github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
+github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -84,8 +85,9 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
@@ -117,8 +119,10 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
-github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
-github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d h1:uFg0KexbouyuGSzmcU9bp/Y7Ml4NXNYjqsRcF/1hnpk=
+github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d/go.mod h1:nupI7yU3J4wEdsa/dUte/z9apDKkUWeRXlK4dGfntcY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -198,6 +202,8 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
@@ -218,6 +224,8 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
+github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
+github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -278,8 +286,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
-github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
-github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -293,8 +301,8 @@ github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnu
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
-github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
@@ -337,6 +345,7 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
@@ -346,8 +355,12 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -399,13 +412,14 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
-golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -415,8 +429,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
-golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
+golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
+golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -434,8 +448,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
-golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
+golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -448,8 +462,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -483,8 +497,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -492,8 +506,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
-golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
@@ -508,8 +522,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
-golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/main.go b/main.go
index 0fccecb6..dba18f22 100644
--- a/main.go
+++ b/main.go
@@ -43,7 +43,7 @@ func Config() config.Config {
}
func MariaDB(cfg config.Config) *mysql.DB {
- migrate := flag.Bool("migrate", false, "perform database migration")
+ migrate := flag.Bool("migrate", false, "perform mysql migration")
flag.Parse()
if *migrate {
migrator.New(migrator.Config{
diff --git a/patientapp/app.go b/patientapp/app.go
index 29d637af..dce0558e 100644
--- a/patientapp/app.go
+++ b/patientapp/app.go
@@ -1 +1,37 @@
package patientapp
+
+import (
+ "git.gocasts.ir/ebhomengo/niki/patientapp/config"
+ "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic"
+ "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
+ "github.com/labstack/echo/v4"
+)
+
+type Application struct {
+ //Config Config
+ HTTPServer *config.EchoServer
+ DB *mysql.DataBase
+}
+
+func Setup(cfg config.Config, conn *mysql.DataBase) Application {
+
+ e := echo.New()
+
+ server := config.EchoServer{
+ Router: e,
+ Config: cfg,
+ }
+
+ return Application{
+ //Config: config,
+ HTTPServer: &server,
+ DB: conn,
+ }
+}
+
+func (a Application) Start() {
+
+ server := analytic.NewServer(a.HTTPServer)
+
+ _ = server.Serve()
+}
diff --git a/patientapp/cmd/main.go b/patientapp/cmd/main.go
new file mode 100644
index 00000000..0c28ac82
--- /dev/null
+++ b/patientapp/cmd/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "time"
+
+ "git.gocasts.ir/ebhomengo/niki/patientapp"
+ "git.gocasts.ir/ebhomengo/niki/patientapp/config"
+ "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
+)
+
+func main() {
+ db := mysql.DataBase{}
+
+ cfg := config.Config{
+ Port: 8080,
+ Cors: config.Cors{
+ AllowOrigins: []string{"*"},
+ },
+ ShutDownCtxTimeout: 5 * time.Second,
+ }
+
+ app := patientapp.Setup(cfg, &db)
+
+ app.Start()
+
+}
diff --git a/patientapp/config/config.go b/patientapp/config/config.go
new file mode 100644
index 00000000..de2c385c
--- /dev/null
+++ b/patientapp/config/config.go
@@ -0,0 +1,22 @@
+package config
+
+import (
+ "time"
+
+ "github.com/labstack/echo/v4"
+)
+
+type Config struct {
+ Port int `koanf:"port"`
+ Cors Cors `koanf:"cors"`
+ ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
+}
+
+type Cors struct {
+ AllowOrigins []string `koanf:"allow_origins"`
+}
+
+type EchoServer struct {
+ Router *echo.Echo
+ Config Config
+}
diff --git a/patientapp/delivery/http/analytic/handler.go b/patientapp/delivery/http/analytic/handler.go
new file mode 100644
index 00000000..fbe7d3c4
--- /dev/null
+++ b/patientapp/delivery/http/analytic/handler.go
@@ -0,0 +1,65 @@
+package analytic
+
+import (
+ "net/http"
+
+ svc "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
+ richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
+ "github.com/labstack/echo/v4"
+)
+
+type Handler struct {
+ service svc.Service
+}
+
+func NewHandler(service svc.Service) *Handler {
+ return &Handler{
+ service: service,
+ }
+}
+
+func (h *Handler) Health(e echo.Context) error {
+ return e.JSON(http.StatusOK, map[string]interface{}{"status": "ok"})
+}
+
+func (h *Handler) PatientsAnalytic(e echo.Context) error {
+ var req svc.ListPatientAnalyticRequest
+
+ richErr := richerror.New(richerror.Op("fetchingPatientList.PatientsAnalytic"))
+
+ if err := e.Bind(&req); err != nil {
+ richErr = richErr.WithErr(err)
+ richErr = richErr.WithKind(1)
+ return echo.NewHTTPError(http.StatusBadRequest, richErr)
+ }
+
+ response, err := h.service.List(e.Request().Context(), req)
+ if err != nil {
+ richErr = richErr.WithErr(err)
+ richErr = richErr.WithKind(4)
+ return echo.NewHTTPError(http.StatusBadRequest, richErr)
+ }
+
+ return e.JSON(http.StatusOK, response)
+}
+
+func (h *Handler) PatientsMapSummary(e echo.Context) error {
+ richErr := richerror.New(richerror.Op("fetchingPatientMapSummary.PatientsMapSummary"))
+
+ var req svc.GetPatientMapSummaryRequest
+
+ if err := e.Bind(&req); err != nil {
+ richErr = richErr.WithErr(err)
+ richErr = richErr.WithKind(1)
+ return echo.NewHTTPError(http.StatusBadRequest, richErr)
+ }
+
+ resp, svcErr := h.service.GetMapSummary(e.Request().Context(), req)
+ if svcErr != nil {
+ richErr = richErr.WithErr(svcErr)
+ richErr = richErr.WithKind(4)
+ return echo.NewHTTPError(http.StatusBadRequest, richErr)
+ }
+
+ return e.JSON(http.StatusOK, resp)
+}
diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go
new file mode 100644
index 00000000..c6499428
--- /dev/null
+++ b/patientapp/delivery/http/analytic/router.go
@@ -0,0 +1,22 @@
+package analytic
+
+import (
+ "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
+ analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
+ "github.com/labstack/echo/v4"
+)
+
+func NewPatientAnalyticRouter(s *echo.Group) {
+
+ mysqlRepo := mysql.NewPatientRepo()
+ //rpcRepo := grpc.NewPatientRepo()
+
+ analyticService := analytic2.NewPatientAnalyticService(mysqlRepo)
+
+ h := NewHandler(analyticService)
+
+ s.GET("/patients", h.PatientsAnalytic)
+ s.GET("/patients-summary", h.PatientsMapSummary)
+ s.GET("/health", h.Health)
+
+}
diff --git a/patientapp/delivery/http/analytic/server.go b/patientapp/delivery/http/analytic/server.go
new file mode 100644
index 00000000..91ec8ba7
--- /dev/null
+++ b/patientapp/delivery/http/analytic/server.go
@@ -0,0 +1,41 @@
+package analytic
+
+import (
+ "context"
+ "fmt"
+
+ "git.gocasts.ir/ebhomengo/niki/patientapp/config"
+)
+
+type Server struct {
+ HTTPServer *config.EchoServer
+}
+
+func NewServer(server *config.EchoServer) *Server {
+
+ return &Server{
+ HTTPServer: server,
+ }
+}
+
+func (s Server) Serve() error {
+ s.RegisterRoutes()
+ // Start server
+ return s.HTTPServer.Router.Start(fmt.Sprintf(":%d", s.HTTPServer.Config.Port))
+}
+
+func (s Server) Stop(ctx context.Context) error {
+ return s.HTTPServer.Router.Shutdown(ctx)
+
+}
+
+func (s Server) RegisterRoutes() {
+
+ v1 := s.HTTPServer.Router.Group("/v1")
+ {
+ // Analytic Group
+ analyticGroup := v1.Group("/analytic")
+ NewPatientAnalyticRouter(analyticGroup)
+ }
+
+}
diff --git a/patientapp/repository/database/.gitkeep b/patientapp/repository/database/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/patientapp/repository/grpc/analytic_repo.go b/patientapp/repository/grpc/analytic_repo.go
new file mode 100644
index 00000000..fe598750
--- /dev/null
+++ b/patientapp/repository/grpc/analytic_repo.go
@@ -0,0 +1,35 @@
+package grpc
+
+import (
+ "context"
+
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
+)
+
+type AnalyticRepository struct{}
+
+func NewPatientRepo() *AnalyticRepository {
+
+ return &AnalyticRepository{}
+}
+
+func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
+
+ return nil, nil
+}
+
+func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
+
+ return 0, nil
+}
+
+func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
+
+ return nil, nil
+}
+
+func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
+
+ return nil, nil
+}
diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go
new file mode 100644
index 00000000..223080cf
--- /dev/null
+++ b/patientapp/repository/mysql/analytic_repo.go
@@ -0,0 +1,36 @@
+package mysql
+
+import (
+ "context"
+
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
+)
+
+type DataBase struct{}
+
+func NewPatientRepo() *DataBase {
+
+ return &DataBase{}
+}
+
+func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
+
+ return nil, nil
+
+}
+
+func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
+
+ return 0, nil
+}
+
+func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
+
+ return nil, nil
+}
+
+func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
+
+ return nil, nil
+}
diff --git a/patientapp/service/analytic/.gitkeep b/patientapp/service/analytic/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/patientapp/service/analytic/helper.go b/patientapp/service/analytic/helper.go
new file mode 100644
index 00000000..94b87fa5
--- /dev/null
+++ b/patientapp/service/analytic/helper.go
@@ -0,0 +1,43 @@
+package analytic
+
+import (
+ "fmt"
+ "github.com/jalaali/go-jalaali"
+ "time"
+)
+
+func normalizeLimitOffset(limit, offset int) (int, int) {
+ if limit <= 0 {
+ limit = 50
+ }
+ if limit > 100 {
+ limit = 100
+ }
+ if offset < 0 {
+ offset = 0
+ }
+ return limit, offset
+}
+
+// convert age range -> DOB range
+func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *string) {
+ if maxAge != nil {
+ t := now.AddDate(-(*maxAge + 1), 0, 1)
+ jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
+ if err != nil {
+ }
+ s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
+ dobFrom = &s
+ }
+
+ if minAge != nil {
+ t := now.AddDate(-*minAge, 0, 0)
+ jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
+ if err != nil {
+ }
+ s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
+ dobTo = &s
+ }
+
+ return
+}
diff --git a/patientapp/service/analytic/param.go b/patientapp/service/analytic/param.go
new file mode 100644
index 00000000..da14af37
--- /dev/null
+++ b/patientapp/service/analytic/param.go
@@ -0,0 +1,71 @@
+package analytic
+
+import (
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
+)
+
+type ListPatientAnalyticRequest struct {
+ // All fields are optional
+ MinAge *int `query:"minAge,omitempty"`
+ MaxAge *int `query:"maxAge,omitempty"`
+ Sex *entity.Sex `query:"sex,omitempty"`
+
+ City *int64 `query:"city,omitempty"`
+ Province *int64 `query:"province,omitempty"`
+
+ Search *string `query:"search,omitempty"`
+
+ Pagination *Pagination `query:"pagination,omitempty"`
+}
+
+type PatientAnalyticItem struct {
+ ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"Last_name"`
+ DateOfBirth string `json:"dob,omitempty"`
+ Sex entity.Sex `json:"sex"`
+ Phone string `json:"phone"`
+ Address entity.Address `json:"address"`
+}
+type PatientAnalyticResponse struct {
+ Items []PatientAnalyticItem `json:"items"`
+ Pagination *Pagination `json:"pagination"`
+ Total int `json:"total"`
+}
+
+func ToPatientResponse(patient entity.Patient) PatientAnalyticItem {
+ return PatientAnalyticItem{
+ ID: patient.ID,
+ FirstName: patient.FirstName,
+ LastName: patient.LastName,
+ DateOfBirth: patient.DateOfBirth,
+ Sex: patient.Sex,
+ Phone: patient.Phone,
+ Address: entity.Address{
+ ProvinceID: patient.Address.ProvinceID,
+ CityID: patient.Address.CityID,
+ },
+ }
+}
+
+// GetPatientMapSummaryRequest =========================== Map ==================================
+type GetPatientMapSummaryRequest struct {
+ Level entity.MapLevel `query:"level"`
+ ParentID *int `query:"parentID"`
+
+ MinAge *int `query:"minAge,omitempty"`
+ MaxAge *int `query:"maxAge,omitempty"`
+ Sex *entity.Sex `query:"sex,omitempty"`
+ Search *string `query:"search,omitempty"`
+}
+
+type GetPatientMapSummaryResponse struct {
+ Level entity.MapLevel `json:"level"`
+ Items map[uint][]entity.MapSummaryItem `json:"items"`
+}
+
+// Pagination ================================ Pagination =============================
+type Pagination struct {
+ Limit int `query:"limit,omitempty"`
+ Offset int `query:"offset,omitempty"`
+}
diff --git a/patientapp/service/analytic/patient_filter.go b/patientapp/service/analytic/patient_filter.go
new file mode 100644
index 00000000..c7dd7695
--- /dev/null
+++ b/patientapp/service/analytic/patient_filter.go
@@ -0,0 +1,27 @@
+package analytic
+
+import (
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
+)
+
+type PatientFilter struct {
+ DOBFrom *string // born after
+ DOBTo *string // born before
+ Sex *entity.Sex
+
+ City *int64
+ Province *int64
+ Country *int64
+
+ Search *string
+
+ Limit int
+ Offset int
+}
+
+type PatientMapFilter struct {
+ MinDOB *string
+ MaxDOB *string
+ Sex *entity.Sex
+ Search *string
+}
diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go
new file mode 100644
index 00000000..518d8756
--- /dev/null
+++ b/patientapp/service/analytic/service.go
@@ -0,0 +1,119 @@
+package analytic
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
+)
+
+var (
+ ErrInvalidProvinceID = errors.New("invalid province id")
+ ErrInvalidCountryID = errors.New("invalid country id")
+ ErrInvalidMapLevel = errors.New("invalid map level")
+)
+
+type Repository interface {
+ GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error)
+ CountPatients(ctx context.Context, f PatientFilter) (int, error)
+
+ SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
+ SummaryByProvince(ctx context.Context, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
+}
+
+type Service struct {
+ repository Repository
+}
+
+func NewPatientAnalyticService(repo Repository) Service {
+ return Service{
+ repository: repo,
+ }
+}
+
+func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) {
+
+ limit, offset := normalizeLimitOffset(req.Pagination.Limit, req.Pagination.Offset)
+
+ // convert age range
+ dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
+
+ filter := PatientFilter{
+ DOBFrom: dobFrom,
+ DOBTo: dobTo,
+ Sex: req.Sex,
+ City: req.City,
+ Province: req.Province,
+ Search: req.Search,
+ Limit: limit,
+ Offset: offset,
+ }
+
+ items, err := s.repository.GetPatients(ctx, filter)
+ if err != nil {
+ return PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err)
+ }
+
+ total, err := s.repository.CountPatients(ctx, filter)
+ if err != nil {
+ return PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err)
+ }
+
+ // mapping response
+ out := make([]PatientAnalyticItem, 0, len(items))
+ for _, value := range items {
+ out = append(out, ToPatientResponse(value))
+ }
+
+ return PatientAnalyticResponse{
+ Items: out,
+ Pagination: &Pagination{
+ Limit: limit,
+ Offset: offset,
+ },
+ Total: total,
+ }, nil
+
+}
+
+func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequest) (GetPatientMapSummaryResponse, error) {
+
+ dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
+
+ filter := PatientMapFilter{
+ MinDOB: dobFrom,
+ MaxDOB: dobTo,
+ Sex: req.Sex,
+ Search: req.Search,
+ }
+
+ switch req.Level {
+ case entity.MapLevelCity:
+ if req.ParentID == nil || *req.ParentID <= 0 {
+ return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID
+ }
+
+ items, err := s.repository.SummaryByCity(ctx, uint(*req.ParentID), filter)
+ if err != nil {
+ return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err)
+ }
+ return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
+
+ case entity.MapLevelProvince:
+ if req.ParentID == nil || *req.ParentID <= 0 {
+ return GetPatientMapSummaryResponse{}, ErrInvalidCountryID
+ }
+
+ items, err := s.repository.SummaryByProvince(ctx, filter)
+ if err != nil {
+ return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err)
+ }
+ return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
+
+ default:
+ return GetPatientMapSummaryResponse{}, ErrInvalidMapLevel
+ }
+
+}
diff --git a/patientapp/service/entity/address.go b/patientapp/service/entity/address.go
new file mode 100644
index 00000000..193531a9
--- /dev/null
+++ b/patientapp/service/entity/address.go
@@ -0,0 +1,28 @@
+package entity
+
+type Address struct {
+ ID uint
+ PostalCode string
+ Address string
+ Name string
+ Lat float64
+ Lon float64
+ CityID uint
+ ProvinceID uint
+}
+
+type AddressAggregated struct {
+ Address Address
+ Province Province
+ City City
+}
+
+type Province struct {
+ ID uint
+ Name string
+}
+type City struct {
+ ID uint
+ Name string
+ ProvinceID uint
+}
diff --git a/patientapp/service/entity/map_summary.go b/patientapp/service/entity/map_summary.go
new file mode 100644
index 00000000..fcfd1284
--- /dev/null
+++ b/patientapp/service/entity/map_summary.go
@@ -0,0 +1,17 @@
+package entity
+
+type MapLevel string
+
+const (
+ MapLevelCity MapLevel = "city"
+ MapLevelProvince MapLevel = "province"
+ MapLevelCountry MapLevel = "country"
+)
+
+type MapSummaryItem struct {
+ LocationID int64
+ Name string
+ Count int
+ CentroidLat float64
+ CentroidLng float64
+}
diff --git a/patientapp/service/entity/patient.go b/patientapp/service/entity/patient.go
new file mode 100644
index 00000000..33857e26
--- /dev/null
+++ b/patientapp/service/entity/patient.go
@@ -0,0 +1,62 @@
+package entity
+
+type Patient struct {
+ ID int64
+ FirstName string
+ LastName string
+ DateOfBirth string
+ Sex Sex
+ Phone string
+ Address Address
+ CaseStatus CaseStatus
+ ReferralSource ReferralSource
+ AssignedStaffId int64
+ StartDate string
+ EndDate string
+}
+
+// Sex ================================== Sex type ==========================================
+type Sex string
+
+const (
+ SexUnknown Sex = "unknown"
+ SexMale Sex = "male"
+ SexFemale Sex = "female"
+ SexOther Sex = "other"
+)
+
+func (s Sex) SexValidation() bool {
+ switch s {
+ case SexUnknown, SexMale, SexFemale, SexOther:
+ return true
+ default:
+ return false
+ }
+}
+
+// CaseStatus =================================== Case Status =======================================
+type CaseStatus string
+
+const (
+ Open CaseStatus = "open"
+ Close CaseStatus = "close"
+ InProgress CaseStatus = "inProgress"
+)
+
+func (s CaseStatus) CaseStatusValidation() bool {
+ switch s {
+ case Open, Close, InProgress:
+ return true
+ default:
+ return false
+ }
+}
+
+// ReferralSource =================================== Referral source =======================================
+type ReferralSource string
+
+const (
+ Hospital ReferralSource = "hospital"
+ Community ReferralSource = "community"
+ Other ReferralSource = "other"
+)
diff --git a/pkg/cfg_loader/cfg_loader.go b/pkg/cfg_loader/cfg_loader.go
new file mode 100644
index 00000000..21116577
--- /dev/null
+++ b/pkg/cfg_loader/cfg_loader.go
@@ -0,0 +1,62 @@
+package cfgloader
+
+import (
+ "github.com/knadh/koanf"
+ "log"
+ "strings"
+
+ "github.com/knadh/koanf/parsers/yaml"
+ "github.com/knadh/koanf/providers/env"
+ "github.com/knadh/koanf/providers/file"
+ //"github.com/knadh/koanf/v2"
+)
+
+type Option struct {
+ Prefix string
+ Delimiter string
+ Separator string
+ YamlFilePath string
+ CallbackEnv func(string) string
+}
+
+// defaultCallbackEnv processes environment variable keys based on provided prefix and separator
+func defaultCallbackEnv(source, prefix, separator string) string {
+ base := strings.ToLower(strings.TrimPrefix(source, prefix))
+ return strings.ReplaceAll(base, separator, ".")
+}
+
+// Load function loads configuration from YAML file and environment variables based on provided options
+func Load(options Option, config interface{}) error {
+ k := koanf.New(options.Delimiter)
+
+ // Load configuration from YAML file if provided
+ if options.YamlFilePath != "" {
+ if err := k.Load(file.Provider(options.YamlFilePath), yaml.Parser()); err != nil {
+ log.Fatalf("Error loading config file: %v", err)
+ return err
+ }
+ }
+
+ // Define callback function for environment variables
+ callback := options.CallbackEnv
+ if callback == nil {
+ // Set default callback using the prefix and separator from options
+ callback = func(source string) string {
+ return defaultCallbackEnv(source, options.Prefix, options.Separator)
+ }
+ }
+
+ // Load environment variables with the specified prefix and callback
+ if err := k.Load(env.Provider(options.Prefix, options.Delimiter, callback), nil); err != nil {
+ log.Fatalf("Error loading environment variables: %v", err)
+ return err
+ }
+
+ // Unmarshal into provided config structure (passing address)
+ if err := k.Unmarshal("", &config); err != nil {
+ log.Fatalf("Error unmarshalling config: %v", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/pkg/database/migrator/migrator.go b/pkg/database/migrator/migrator.go
new file mode 100644
index 00000000..3a1dde0b
--- /dev/null
+++ b/pkg/database/migrator/migrator.go
@@ -0,0 +1,69 @@
+package migrator
+
+import (
+ "database/sql"
+ "fmt"
+
+ "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
+ migrate "github.com/rubenv/sql-migrate"
+)
+
+type Config struct {
+ MysqlConfig mysql.Config
+ MigrationPath string
+ MigrationDBName string
+}
+
+type Migrator struct {
+ cfg Config
+ dialect string
+ migrations *migrate.FileMigrationSource
+}
+
+// TODO - set migration table name
+// TODO - add limit to Up and Down method
+
+func New(cfg Config) Migrator {
+ // OR: Read migrations from a folder:
+ migrations := &migrate.FileMigrationSource{
+ Dir: cfg.MigrationPath,
+ }
+
+ return Migrator{cfg: cfg, dialect: "mysql", migrations: migrations}
+}
+
+func (m Migrator) Up() {
+ migrate.SetTable(m.cfg.MigrationDBName)
+
+ db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
+ m.cfg.MysqlConfig.Username, m.cfg.MysqlConfig.Password, m.cfg.MysqlConfig.Host, m.cfg.MysqlConfig.Port, m.cfg.MysqlConfig.DBName))
+ if err != nil {
+ panic(fmt.Errorf("can't open mysql db: %w", err))
+ }
+
+ n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Up)
+ if err != nil {
+ panic(fmt.Errorf("can't apply migrations: %w", err))
+ }
+ fmt.Printf("Applied %d migrations!\n", n)
+}
+
+func (m Migrator) Down() {
+ migrate.SetTable(m.cfg.MigrationDBName)
+
+ db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
+ m.cfg.MysqlConfig.Username, m.cfg.MysqlConfig.Password, m.cfg.MysqlConfig.Host, m.cfg.MysqlConfig.Port, m.cfg.MysqlConfig.DBName))
+ if err != nil {
+ panic(fmt.Errorf("can't open mysql db: %w", err))
+ }
+
+ n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Down)
+ if err != nil {
+ panic(fmt.Errorf("can't rollback migrations: %w", err))
+ }
+ fmt.Printf("Rollbacked %d migrations!\n", n)
+}
+
+func (m Migrator) Status() {
+ // TODO - add status
+}
diff --git a/pkg/database/mysql/db.go b/pkg/database/mysql/db.go
new file mode 100644
index 00000000..dc2f3dad
--- /dev/null
+++ b/pkg/database/mysql/db.go
@@ -0,0 +1,90 @@
+package mysql
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ querier "git.gocasts.ir/ebhomengo/niki/pkg/query_transaction/sql"
+ _ "github.com/go-sql-driver/mysql"
+ "sync"
+ "time"
+)
+
+type Config struct {
+ Username string `koanf:"username"`
+ Password string `koanf:"password"`
+ Port int `koanf:"port"`
+ Host string `koanf:"host"`
+ DBName string `koanf:"db_name"`
+}
+
+type DB struct {
+ config Config
+ db *querier.SQLDB
+ mu sync.Mutex
+ statements map[statementKey]*sql.Stmt
+}
+
+func (db *DB) Conn() *querier.SQLDB {
+ return db.db
+}
+
+// TODO: this temporary to ignore linter error (magic number).
+const (
+ dbMaxConnLifetime = time.Minute * 3
+ dbMaxOpenConns = 10
+ dbMaxIdleConns = 10
+)
+
+func New(config Config) *DB {
+ // parseTime=true changes the output type of DATE and DATETIME values to time.Time
+ // instead of []byte / string
+ // The date or datetime like 0000-00-00 00:00:00 is converted into zero value of time.Time
+ db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
+ config.Username, config.Password, config.Host, config.Port, config.DBName))
+ if err != nil {
+ panic(fmt.Errorf("can't open mysql db: %w", err))
+ }
+
+ // See "Important settings" section.
+ db.SetConnMaxLifetime(dbMaxConnLifetime)
+ db.SetMaxOpenConns(dbMaxOpenConns)
+ db.SetMaxIdleConns(dbMaxIdleConns)
+
+ return &DB{
+ config: config,
+ db: &querier.SQLDB{DB: db},
+ statements: make(map[statementKey]*sql.Stmt),
+ }
+}
+
+func (db *DB) PrepareStatement(ctx context.Context, key statementKey, query string) (*sql.Stmt, error) {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ if stmt, ok := db.statements[key]; ok {
+ return stmt, nil
+ }
+
+ stmt, err := db.db.PrepareContext(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+ db.statements[key] = stmt
+
+ return stmt, nil
+}
+
+func (db *DB) CloseStatements() error {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ for _, stmt := range db.statements {
+ err := stmt.Close()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/database/mysql/dbconfig.yml b/pkg/database/mysql/dbconfig.yml
new file mode 100644
index 00000000..db89d8a3
--- /dev/null
+++ b/pkg/database/mysql/dbconfig.yml
@@ -0,0 +1,5 @@
+production:
+ dialect: mysql
+ datasource: niki:nikiappt0lk2o20@(localhost:3306)/niki_db?parseTime=true
+ dir: pkg/database/mysql/migration
+ table: gorp_migrations
diff --git a/pkg/database/mysql/prepared_statement.go b/pkg/database/mysql/prepared_statement.go
new file mode 100644
index 00000000..8ec395ed
--- /dev/null
+++ b/pkg/database/mysql/prepared_statement.go
@@ -0,0 +1,52 @@
+package mysql
+
+type statementKey uint
+
+const (
+ StatementKeyAddressCreateForBenefactor statementKey = iota + 1
+ StatementKeyAddressDeleteForBenefactor
+ StatementKeyAddressIsExistByID
+ StatementKeyAddressGetByID
+ StatementKeyAddressGetAllByBenefactorID
+ StatementKeyAddressUpdate
+ StatementKeyCityGetProvinceIDByID
+ StatementKeyCityIsExistByID
+ StatementKeyCityGetAll
+ StatementKeyProvinceIsExistByID
+ StatementKeyProvinceGetAll
+ StatementKeyAdminAccessControlGetPermissions
+ StatementKeyAdminAdd
+ StatementKeyAdminExistByEmail
+ StatementKeyAdminExistByPhoneNumber
+ StatementKeyAdminAgentExistByID
+ StatementKeyAdminGetByID
+ StatementKeyAdminGetByPhoneNumber
+ StatementKeyAdminAgentGetAll
+ StatementKeyBenefactorGetByID
+ StatementKeyBenefactorGetByIDs
+ StatementKeyBenefactorGetByPhoneNumber
+ StatementKeyBenefactorCreate
+ StatementKeyBenefactorGetAll
+ StatementKeyKindBoxAdd
+ StatementKeyKindBoxAssignReceiverAgent
+ StatementKeyKindBoxEnumerate
+ StatementKeyKindBoxExistForBenefactor
+ StatementKeyKindBoxGetByID
+ StatementKeyKindBoxGetAwaitingReturnByAgent
+ StatementKeyKindBoxRegisterEmptyingRequest
+ StatementKeyKindBoxReturn
+ StatementKeyKindBoxReqAccept
+ StatementKeyKindBoxReqAdd
+ StatementKeyKindBoxReqAssignSenderAgent
+ StatementKeyKindBoxReqDeleteByID
+ StatementKeyKindBoxReqGetByID
+ StatementKeyKindBoxReqGetAwaitingDeliveryByAgent
+ StatementKeyKindBoxReqRollbackToPendingStatus
+ StatementKeyKindBoxReqReject
+ StatementKeyKindBoxReqUpdate
+ StatementKeyKindBoxUpdate
+ StatementKeyReferTimeGetByID
+ StatementKeyReferTimeGetAll
+ StatementKeyBenefactorUpdate
+ StatementKeyBenefactorUpdateStatus
+)
diff --git a/pkg/database/mysql/scanner.go b/pkg/database/mysql/scanner.go
new file mode 100644
index 00000000..0fe80534
--- /dev/null
+++ b/pkg/database/mysql/scanner.go
@@ -0,0 +1,5 @@
+package mysql
+
+type Scanner interface {
+ Scan(dest ...any) error
+}
diff --git a/pkg/database/redis/otp/db.go b/pkg/database/redis/otp/db.go
new file mode 100644
index 00000000..a641cc7c
--- /dev/null
+++ b/pkg/database/redis/otp/db.go
@@ -0,0 +1,11 @@
+package redisotp
+
+import "git.gocasts.ir/ebhomengo/niki/adapter/redis"
+
+type DB struct {
+ adapter *redis.Adapter
+}
+
+func New(adapter *redis.Adapter) *DB {
+ return &DB{adapter: adapter}
+}
diff --git a/pkg/date_parser/date_parser.go b/pkg/date_parser/date_parser.go
new file mode 100644
index 00000000..5b89e6ff
--- /dev/null
+++ b/pkg/date_parser/date_parser.go
@@ -0,0 +1,19 @@
+package date_parser
+
+import (
+ "fmt"
+ "time"
+)
+
+// ParseDate parses a date string in "YYYY-MM-DD" format and returns a time.Time object
+func ParseDate(input string) (time.Time, error) {
+ const layout = "2006-01-02"
+
+ // Parse the input string
+ convertedDate, err := time.Parse(layout, input)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("invalid date format: %v", err)
+ }
+
+ return convertedDate, nil
+}
diff --git a/pkg/httpserver/constant.go b/pkg/httpserver/constant.go
new file mode 100644
index 00000000..30be3c74
--- /dev/null
+++ b/pkg/httpserver/constant.go
@@ -0,0 +1,7 @@
+package httpserver
+
+import "time"
+
+const (
+ DefaultShutdownTimeout = 10 * time.Second
+)
diff --git a/pkg/httpserver/server.go b/pkg/httpserver/server.go
new file mode 100644
index 00000000..51abd3fe
--- /dev/null
+++ b/pkg/httpserver/server.go
@@ -0,0 +1,121 @@
+package httpserver
+
+import (
+ "context"
+ "fmt"
+ echomiddleware "github.com/gocasters/rankr/pkg/echo_middleware"
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+ "strings"
+ "sync"
+ "time"
+)
+
+type Config struct {
+ Host string `koanf:"host"`
+ Port int `koanf:"port"`
+ CORS CORS `koanf:"cors"`
+ ShutdownTimeout time.Duration `koanf:"shutdown_context_timeout"`
+ HideBanner bool `koanf:"hide_banner"`
+ HidePort bool `koanf:"hide_port"`
+ PublicPaths []string `koanf:"public_paths"`
+
+ // Optional Otel middleware can be injected from outside.
+ OtelMiddleware echo.MiddlewareFunc
+}
+
+type CORS struct {
+ AllowOrigins []string `koanf:"allow_origins"`
+}
+
+type Server struct {
+ router *echo.Echo
+ config *Config
+
+ requireClaimsOnce sync.Once
+}
+
+var basePublicPaths = []string{
+ "/v1/login",
+ "/v1/refresh-token",
+ "/v1/me",
+ "/ping",
+ "/ping-otel",
+}
+
+func New(cfg Config) (*Server, error) {
+ if cfg.Port < 1 || cfg.Port > 65535 {
+ return nil, fmt.Errorf("invalid port: %d", cfg.Port)
+ }
+
+ if cfg.ShutdownTimeout <= 0 {
+ cfg.ShutdownTimeout = DefaultShutdownTimeout
+ }
+
+ e := echo.New()
+
+ if cfg.OtelMiddleware != nil {
+ e.Use(cfg.OtelMiddleware)
+ }
+
+ e.Use(middleware.RequestID())
+ e.Use(middleware.Logger())
+ e.Use(middleware.Recover())
+ e.Use(
+ middleware.CORSWithConfig(
+ middleware.CORSConfig{
+ AllowOrigins: cfg.CORS.AllowOrigins,
+ },
+ ),
+ )
+
+ return &Server{
+ router: e,
+ config: &cfg,
+ }, nil
+}
+
+func (s *Server) GetRouter() *echo.Echo {
+ s.requireClaimsOnce.Do(func() {
+ s.router.Use(
+ echomiddleware.RequireUserInfo(
+ echomiddleware.RequireUserInfoOptions{
+ Skipper: newPublicPathSkipper(s.config.PublicPaths...),
+ },
+ ),
+ )
+ })
+
+ return s.router
+}
+
+func (s *Server) GetConfig() *Config {
+ return s.config
+}
+
+func (s *Server) Start() error {
+ addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
+
+ s.router.HideBanner = s.config.HideBanner
+ s.router.HidePort = s.config.HidePort
+
+ return s.router.Start(addr)
+}
+
+func (s *Server) Stop(ctx context.Context) error {
+ return s.router.Shutdown(ctx)
+}
+
+func newPublicPathSkipper(extraPaths ...string) middleware.Skipper {
+ paths := make([]string, 0, len(basePublicPaths)+len(extraPaths))
+ paths = append(paths, basePublicPaths...)
+ paths = append(paths, extraPaths...)
+ staticPublicPathSkipper := echomiddleware.SkipExactPaths(paths...)
+
+ return func(c echo.Context) bool {
+ path := strings.TrimSuffix(c.Request().URL.Path, "/")
+ return staticPublicPathSkipper(c) ||
+ strings.HasSuffix(path, "/health-check") ||
+ strings.HasSuffix(path, "/health_check")
+ }
+}
diff --git a/pkg/httpserver/server_test.go b/pkg/httpserver/server_test.go
new file mode 100644
index 00000000..f54ebb37
--- /dev/null
+++ b/pkg/httpserver/server_test.go
@@ -0,0 +1,190 @@
+package httpserver_test
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/gocasters/rankr/pkg/authhttp"
+ "github.com/gocasters/rankr/pkg/httpserver"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+)
+
+// TestNew tests the constructor function for various scenarios.
+func TestNew(t *testing.T) {
+ t.Run("successful creation with valid config", func(t *testing.T) {
+ // Arrange
+ cfg := httpserver.Config{
+ Port: 8080,
+ ShutdownTimeout: 5 * time.Second,
+ }
+
+ // Act
+ server, err := httpserver.New(cfg)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, server)
+ assert.NotNil(t, server.GetRouter(), "GetRouter should return a non-nil router")
+ })
+
+ t.Run("error on invalid port", func(t *testing.T) {
+ // Arrange
+ cfg := httpserver.Config{Port: 0} // Invalid port
+
+ // Act
+ server, err := httpserver.New(cfg)
+
+ // Assert
+ assert.Error(t, err)
+ assert.Nil(t, server)
+ assert.Contains(t, err.Error(), "invalid port")
+ })
+
+ t.Run("sets default shutdown timeout", func(t *testing.T) {
+ // Arrange
+ cfg := httpserver.Config{
+ Port: 8080,
+ ShutdownTimeout: 0, // No timeout provided
+ }
+
+ // Act
+ server, err := httpserver.New(cfg)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, server)
+ assert.Equal(t, httpserver.DefaultShutdownTimeout, server.GetConfig().ShutdownTimeout, "Default timeout should be set")
+ })
+}
+
+// TestOtelMiddlewareInjection verifies that optional middleware is correctly added.
+func TestOtelMiddlewareInjection(t *testing.T) {
+ // Arrange
+ middlewareWasCalled := false
+ mockOtelMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ middlewareWasCalled = true
+ return next(c)
+ }
+ }
+
+ cfg := httpserver.Config{
+ Port: 8080,
+ OtelMiddleware: mockOtelMiddleware,
+ }
+
+ server, err := httpserver.New(cfg)
+ assert.NoError(t, err)
+
+ server.GetRouter().GET("/test", func(c echo.Context) error {
+ return c.String(http.StatusOK, "ok")
+ })
+
+ // Act
+ req := httptest.NewRequest(http.MethodGet, "/test", nil)
+ userInfo, encErr := authhttp.EncodeUserInfo("1")
+ assert.NoError(t, encErr)
+ req.Header.Set("X-User-Info", userInfo)
+ rec := httptest.NewRecorder()
+ server.GetRouter().ServeHTTP(rec, req)
+
+ // Assert
+ assert.True(t, middlewareWasCalled, "The injected Otel middleware should have been called")
+}
+
+// TestRouteRegistrationAndResponse confirms that routes can be added and respond correctly.
+func TestRouteRegistrationAndResponse(t *testing.T) {
+ // Arrange
+ server, err := httpserver.New(httpserver.Config{Port: 8080})
+ assert.NoError(t, err)
+
+ expectedResponse := "Hello, Tester!"
+
+ // Act: Register a new GET route using the GetRouter() method.
+ server.GetRouter().GET("/hello", func(c echo.Context) error {
+ return c.String(http.StatusOK, expectedResponse)
+ })
+
+ req := httptest.NewRequest(http.MethodGet, "/hello", nil)
+ userInfo, encErr := authhttp.EncodeUserInfo("1")
+ assert.NoError(t, encErr)
+ req.Header.Set("X-User-Info", userInfo)
+ rec := httptest.NewRecorder()
+ server.GetRouter().ServeHTTP(rec, req)
+
+ // Assert
+ assert.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, expectedResponse, rec.Body.String())
+}
+
+func TestStopWithTimeout(t *testing.T) {
+ // Arrange
+ server, err := httpserver.New(httpserver.Config{Port: 9090}) // Use a different port to avoid conflicts
+ assert.NoError(t, err)
+
+ // Act: Start the server in a separate goroutine
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- server.Start()
+ }()
+
+ // Stop the server and verify shutdown succeeds
+ stopErr := server.Stop(t.Context())
+ assert.NoError(t, stopErr, "StopWithTimeout should not return an error when stopping a running server")
+
+ // Now verify Start() exited due to shutdown
+ startErr := <-errCh
+ assert.ErrorIs(t, startErr, http.ErrServerClosed, "server.Start() should return ErrServerClosed after shutdown")
+}
+
+func TestPublicRoutesAndProtectedRoutes(t *testing.T) {
+ server, err := httpserver.New(httpserver.Config{Port: 8080})
+ assert.NoError(t, err)
+
+ server.GetRouter().GET("/v1/module/health-check", func(c echo.Context) error {
+ return c.String(http.StatusOK, "ok")
+ })
+ server.GetRouter().POST("/v1/login", func(c echo.Context) error {
+ return c.String(http.StatusOK, "ok")
+ })
+ server.GetRouter().GET("/secure", func(c echo.Context) error {
+ return c.String(http.StatusOK, "ok")
+ })
+
+ healthReq := httptest.NewRequest(http.MethodGet, "/v1/module/health-check", nil)
+ healthRec := httptest.NewRecorder()
+ server.GetRouter().ServeHTTP(healthRec, healthReq)
+ assert.Equal(t, http.StatusOK, healthRec.Code)
+
+ loginReq := httptest.NewRequest(http.MethodPost, "/v1/login", nil)
+ loginRec := httptest.NewRecorder()
+ server.GetRouter().ServeHTTP(loginRec, loginReq)
+ assert.Equal(t, http.StatusOK, loginRec.Code)
+
+ secureReq := httptest.NewRequest(http.MethodGet, "/secure", nil)
+ secureRec := httptest.NewRecorder()
+ server.GetRouter().ServeHTTP(secureRec, secureReq)
+ assert.Equal(t, http.StatusUnauthorized, secureRec.Code)
+}
+
+func TestConfigPublicPathsAreSkipped(t *testing.T) {
+ server, err := httpserver.New(httpserver.Config{
+ Port: 8080,
+ PublicPaths: []string{" v1/public/info/ "},
+ })
+ assert.NoError(t, err)
+
+ server.GetRouter().GET("/v1/public/info", func(c echo.Context) error {
+ return c.String(http.StatusOK, "ok")
+ })
+
+ req := httptest.NewRequest(http.MethodGet, "/v1/public/info", nil)
+ rec := httptest.NewRecorder()
+ server.GetRouter().ServeHTTP(rec, req)
+
+ assert.Equal(t, http.StatusOK, rec.Code)
+}
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
new file mode 100644
index 00000000..fd87c818
--- /dev/null
+++ b/pkg/logger/logger.go
@@ -0,0 +1,84 @@
+package logger
+
+import (
+ "io"
+ "log"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "gopkg.in/natefinch/lumberjack.v2"
+)
+
+var globalLogger *slog.Logger
+var once sync.Once
+
+type Config struct {
+ Level string `koanf:"level"`
+ FilePath string `koanf:"file_path"`
+ UseLocalTime bool `koanf:"use_local_time"`
+ FileMaxSizeInMB int `koanf:"file_max_size_in_mb"`
+ FileMaxAgeInDays int `koanf:"file_max_age_in_days"`
+}
+
+// Init initializes the global logger instance.
+func Init(cfg Config) {
+ once.Do(func() {
+ workingDir, err := os.Getwd()
+ if err != nil {
+ log.Fatalf("Error getting current working directory: %v", err)
+ }
+ fileWriter := &lumberjack.Logger{
+ Filename: filepath.Join(workingDir, cfg.FilePath),
+ LocalTime: cfg.UseLocalTime,
+ MaxSize: cfg.FileMaxSizeInMB,
+ MaxAge: cfg.FileMaxAgeInDays,
+ }
+
+ level := mapLevel(cfg.Level)
+ globalLogger = slog.New(
+ slog.NewJSONHandler(io.MultiWriter(fileWriter, os.Stdout), &slog.HandlerOptions{
+ Level: level,
+ }),
+ )
+ })
+}
+
+// L returns the global logger instance.
+func L() *slog.Logger {
+ return globalLogger
+}
+
+// New creates a new logger instance for each service with specific settings.
+func New(cfg Config) *slog.Logger {
+ workingDir, err := os.Getwd()
+ if err != nil {
+ log.Fatalf("Error getting current working directory: %v", err)
+ }
+
+ fileWriter := &lumberjack.Logger{
+ Filename: filepath.Join(workingDir, cfg.FilePath),
+ LocalTime: cfg.UseLocalTime,
+ MaxSize: cfg.FileMaxSizeInMB,
+ MaxAge: cfg.FileMaxAgeInDays,
+ }
+ return slog.New(
+ slog.NewJSONHandler(io.MultiWriter(fileWriter), &slog.HandlerOptions{}),
+ )
+}
+
+func mapLevel(levelStr string) slog.Level {
+ switch levelStr {
+ case "debug":
+ return slog.LevelDebug
+ case "info":
+ return slog.LevelInfo
+ case "warn":
+ return slog.LevelWarn
+ case "error":
+ return slog.LevelError
+ default:
+ return slog.LevelInfo
+ }
+}
diff --git a/pkg/path/path.go b/pkg/path/path.go
new file mode 100644
index 00000000..a2bfd1a6
--- /dev/null
+++ b/pkg/path/path.go
@@ -0,0 +1,34 @@
+package path
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+)
+
+// PathProjectRoot searches upwards from the current directory to find the project root,
+// identified by the presence of a "go.mod" file.
+func PathProjectRoot() (string, error) {
+ dir, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+
+ for {
+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
+ return dir, nil
+ }
+ if _, err := os.Stat(filepath.Join(dir, "go.work")); err == nil {
+ return dir, nil
+ }
+ if fi, err := os.Stat(filepath.Join(dir, ".git")); err == nil && fi.IsDir() {
+ return dir, nil
+ }
+
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ return "", errors.New("go.mod not found in any parent directory")
+ }
+ dir = parent
+ }
+}
diff --git a/productapp/repository/migrations/1775595704_create_categories_table.sql b/productapp/repository/migrations/1775595704_create_categories_table.sql
new file mode 100644
index 00000000..a1d7701e
--- /dev/null
+++ b/productapp/repository/migrations/1775595704_create_categories_table.sql
@@ -0,0 +1,10 @@
+-- +migrate Up
+CREATE TABLE `categories` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `slug` VARCHAR(255) NOT NULL UNIQUE,
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
+
+-- +migrate Down
+DROP TABLE IF EXISTS `categories`;
diff --git a/productapp/repository/migrations/1775595705_create_products_table.sql b/productapp/repository/migrations/1775595705_create_products_table.sql
new file mode 100644
index 00000000..23e210c1
--- /dev/null
+++ b/productapp/repository/migrations/1775595705_create_products_table.sql
@@ -0,0 +1,17 @@
+-- +migrate Up
+CREATE TABLE `products` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `slug` VARCHAR(255) NOT NULL UNIQUE,
+ `description` TEXT,
+ `price` DECIMAL(10, 2) NOT NULL,
+ `stock` INT DEFAULT 0,
+ `is_active` BOOLEAN DEFAULT TRUE,
+ `features` JSON DEFAULT NULL,
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `deleted_at` TIMESTAMP DEFAULT NULL,
+ FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
+
+-- +migrate Down
+DROP TABLE IF EXISTS `products`;
diff --git a/productapp/repository/migrations/1775595706_create_category_products_table.sql b/productapp/repository/migrations/1775595706_create_category_products_table.sql
new file mode 100644
index 00000000..307e13c7
--- /dev/null
+++ b/productapp/repository/migrations/1775595706_create_category_products_table.sql
@@ -0,0 +1,11 @@
+-- +migrate Up
+CREATE TABLE `category_products` (
+ `category_id` INT NOT NULL,
+ `product_id` INT NOT NULL,
+ PRIMARY KEY (`category_id`, `product_id`),
+ FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE,
+ FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
+
+-- +migrate Down
+DROP TABLE IF EXISTS `category_products`;
diff --git a/productapp/repository/migrations/1775595707_create_product_images_table.sql b/productapp/repository/migrations/1775595707_create_product_images_table.sql
new file mode 100644
index 00000000..ac9e1fb3
--- /dev/null
+++ b/productapp/repository/migrations/1775595707_create_product_images_table.sql
@@ -0,0 +1,11 @@
+-- +migrate Up
+CREATE TABLE `product_images` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `product_id` INT NOT NULL,
+ `image_path` VARCHAR(255) NOT NULL,
+ `is_primary` BOOLEAN DEFAULT FALSE,
+ FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
+
+-- +migrate Down
+DROP TABLE IF EXISTS `product_images`;
diff --git a/productapp/service/category/entity.go b/productapp/service/category/entity.go
new file mode 100644
index 00000000..42169c58
--- /dev/null
+++ b/productapp/service/category/entity.go
@@ -0,0 +1,10 @@
+package category
+
+import "time"
+
+type Category struct {
+ ID uint
+ Name string
+ Slug string
+ CreatedAt time.Time
+}
diff --git a/productapp/service/category/param.go b/productapp/service/category/param.go
new file mode 100644
index 00000000..79661ada
--- /dev/null
+++ b/productapp/service/category/param.go
@@ -0,0 +1 @@
+package category
diff --git a/productapp/service/category/service.go b/productapp/service/category/service.go
new file mode 100644
index 00000000..79661ada
--- /dev/null
+++ b/productapp/service/category/service.go
@@ -0,0 +1 @@
+package category
diff --git a/productapp/service/category/validator.go b/productapp/service/category/validator.go
new file mode 100644
index 00000000..79661ada
--- /dev/null
+++ b/productapp/service/category/validator.go
@@ -0,0 +1 @@
+package category
diff --git a/productapp/service/entity.go b/productapp/service/entity.go
deleted file mode 100644
index 6d43c336..00000000
--- a/productapp/service/entity.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/productapp/service/param.go b/productapp/service/param.go
deleted file mode 100644
index 6d43c336..00000000
--- a/productapp/service/param.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/productapp/service/product/entity.go b/productapp/service/product/entity.go
new file mode 100644
index 00000000..b3dc5901
--- /dev/null
+++ b/productapp/service/product/entity.go
@@ -0,0 +1,31 @@
+package product
+
+import (
+ "database/sql"
+ "time"
+)
+
+type Product struct {
+ ID uint
+ Name string
+ Slug string
+ Description string
+ Price float64
+ Stock int
+ IsActive bool
+ Features string
+ CreatedAt time.Time
+ DeletedAt sql.NullTime
+}
+
+type ProductImage struct {
+ ID uint
+ ProductID uint
+ ImagePath string
+ IsPrimary bool
+}
+
+type CategoryProduct struct {
+ CategoryID uint
+ ProductID uint
+}
diff --git a/productapp/service/product/param.go b/productapp/service/product/param.go
new file mode 100644
index 00000000..e6ae0918
--- /dev/null
+++ b/productapp/service/product/param.go
@@ -0,0 +1 @@
+package product
diff --git a/productapp/service/product/service.go b/productapp/service/product/service.go
new file mode 100644
index 00000000..e6ae0918
--- /dev/null
+++ b/productapp/service/product/service.go
@@ -0,0 +1 @@
+package product
diff --git a/productapp/service/product/validator.go b/productapp/service/product/validator.go
new file mode 100644
index 00000000..e6ae0918
--- /dev/null
+++ b/productapp/service/product/validator.go
@@ -0,0 +1 @@
+package product
diff --git a/productapp/service/service.go b/productapp/service/service.go
deleted file mode 100644
index 6d43c336..00000000
--- a/productapp/service/service.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/productapp/service/validator.go b/productapp/service/validator.go
deleted file mode 100644
index 6d43c336..00000000
--- a/productapp/service/validator.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/purchaseapp/app.go b/purchaseapp/app.go
index 8f3c7051..7f62b636 100644
--- a/purchaseapp/app.go
+++ b/purchaseapp/app.go
@@ -1 +1,61 @@
package purchaseapp
+
+import (
+ "context"
+ "fmt"
+ purchaseHTTP "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http"
+ purchaseHandler "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
+ purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql"
+ purchaseService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
+ "git.gocasts.ir/ebhomengo/niki/repository/mysql"
+)
+
+type Application struct {
+ Config Config
+ HTTPServer *purchaseHTTP.Server
+ purchaseService purchaseService.Service
+ PurchaseHandler *purchaseHandler.Handler
+ PurchaseRepo purchaseService.Repo
+ DB *mysql.DB
+}
+
+func SetUp(ctx context.Context, config Config, DB mysql.DB) *Application {
+
+ //cfg := mysql.Config{
+ // Username: "niki",
+ // Password: "nikiappt0lk2o20",
+ // Port: 3306,
+ // Host: "localhost",
+ // DBName: "niki_db",
+ //}
+ db := mysql.New(config.Mysql)
+ 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)
+ handler := purchaseHandler.New(orderSvc)
+
+ return &Application{
+ Config: Config{},
+ HTTPServer: server,
+ purchaseService: orderSvc,
+ PurchaseHandler: handler,
+ PurchaseRepo: orderRepo,
+ DB: &DB,
+ }
+
+}
+
+func HTTPServer(orderSvc purchaseService.Service) *purchaseHTTP.Server {
+ return purchaseHTTP.New(orderSvc)
+}
+
+func Service(orderRepo *purchaseMysql.DB) purchaseService.Service {
+ return purchaseService.New(orderRepo)
+}
diff --git a/purchaseapp/config.go b/purchaseapp/config.go
index 3957f32e..988edda2 100644
--- a/purchaseapp/config.go
+++ b/purchaseapp/config.go
@@ -1,4 +1,9 @@
package purchaseapp
+import (
+ "git.gocasts.ir/ebhomengo/niki/repository/mysql"
+)
+
type Config struct {
+ Mysql mysql.Config `koanf:"mariadb"`
}
diff --git a/purchaseapp/delivery/http/invoice/handler.go b/purchaseapp/delivery/http/invoice/handler.go
deleted file mode 100644
index d5de3ca8..00000000
--- a/purchaseapp/delivery/http/invoice/handler.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package invoice
-
-type Handler struct {
-}
-
-func New() *Handler {
- return &Handler{}
-}
diff --git a/purchaseapp/delivery/http/invoice/route.go b/purchaseapp/delivery/http/invoice/route.go
deleted file mode 100644
index 7584f63a..00000000
--- a/purchaseapp/delivery/http/invoice/route.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package invoice
-
-import "github.com/labstack/echo/v4"
-
-func (h Handler) SetRoutes(e *echo.Echo) {
-
-}
diff --git a/purchaseapp/delivery/http/order/handler.go b/purchaseapp/delivery/http/order/handler.go
index a515fef5..9ae96087 100644
--- a/purchaseapp/delivery/http/order/handler.go
+++ b/purchaseapp/delivery/http/order/handler.go
@@ -1,7 +1,68 @@
package order
-type Handler struct{}
+import (
+ richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
+ "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
+ "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
+ "github.com/labstack/echo/v4"
+ "net/http"
+ "time"
+)
-func New() *Handler {
- return &Handler{}
+type Handler struct {
+ orderSvc *order.Service
+}
+
+func New(orderSvc order.Service) *Handler {
+ return &Handler{orderSvc: &orderSvc}
+}
+
+func (h *Handler) CreateOrderHandler(c echo.Context) error {
+ var req order.CreateOrderRequest
+ if err := c.Bind(&req); err != nil {
+ msg, code := getErrorDataFromRichError(err)
+ return echo.NewHTTPError(code, msg)
+ }
+
+ orderItems := req.OrderItems
+ order := entity.Order{
+ ID: 0,
+ UserID: req.UserID,
+ TotalAmount: req.TotalAmount,
+ TotalDiscount: req.TotalDiscount,
+ ShippingID: req.ShippingID,
+ PaymentMethod: req.PaymentMethod,
+ ProcessStatus: entity.WaitingToPay,
+ PaymentStatus: entity.UnPaid,
+ Address: req.Address,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+ resp, lErr := h.orderSvc.CreateOrder(order, orderItems)
+
+ if lErr != nil {
+ msg, code := getErrorDataFromRichError(lErr)
+ return echo.NewHTTPError(code, msg)
+ }
+ return c.JSON(http.StatusOK, resp)
+}
+
+func getErrorDataFromRichError(err error) (message string, code int) {
+ switch err.(type) {
+ case richerror.RichError:
+ re := err.(richerror.RichError)
+
+ return re.Message(), mapKindToCode(re.Kind())
+ default:
+ return err.Error(), http.StatusBadRequest
+ }
+}
+
+func mapKindToCode(kind richerror.Kind) int {
+ switch kind {
+ case richerror.KindInvalid:
+ return http.StatusUnprocessableEntity
+ default:
+ return http.StatusBadRequest
+ }
}
diff --git a/purchaseapp/delivery/http/order/route.go b/purchaseapp/delivery/http/order/route.go
index 62b20d3c..3db0dd1f 100644
--- a/purchaseapp/delivery/http/order/route.go
+++ b/purchaseapp/delivery/http/order/route.go
@@ -4,4 +4,5 @@ import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) {
+ e.POST("/order/create", h.CreateOrderHandler)
}
diff --git a/purchaseapp/delivery/http/server.go b/purchaseapp/delivery/http/server.go
index fd23a2f6..1da710cb 100644
--- a/purchaseapp/delivery/http/server.go
+++ b/purchaseapp/delivery/http/server.go
@@ -1,33 +1,32 @@
package http
import (
- httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
- "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/invoice"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
+ orderService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
)
type Server struct {
- HTTPServer *httpserver.Server
- OrderHandler *order.Handler
- InvoiceHandler *invoice.Handler
+ OrderHandler *order.Handler
}
-func New(httpserver *httpserver.Server) *Server {
+func New(orderSvc orderService.Service) *Server {
return &Server{
- HTTPServer: httpserver,
- OrderHandler: order.New(),
- InvoiceHandler: invoice.New(),
+ OrderHandler: order.New(orderSvc),
}
}
func (s *Server) Serve() {
- s.RegisterRoutes()
+ e := echo.New()
+ e.Use(middleware.RequestLogger())
+ e.GET("/purchase/health-check", s.healthCheck)
+
+ s.OrderHandler.SetRoutes(e)
+
+ if err := e.Start(":8088"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
}
func (s *Server) Stop() {}
-
-func (s *Server) RegisterRoutes() {
- s.HTTPServer.Router.GET("/purchase/health-check", s.healthCheck)
- s.OrderHandler.SetRoutes(s.HTTPServer.Router)
- s.InvoiceHandler.SetRoutes(s.HTTPServer.Router)
-}
diff --git a/purchaseapp/entity/invoice.go b/purchaseapp/entity/invoice.go
deleted file mode 100644
index ee532a52..00000000
--- a/purchaseapp/entity/invoice.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package entity
-
-type Invoice struct {
-}
diff --git a/purchaseapp/entity/order.go b/purchaseapp/entity/order.go
index 332403f8..0e235f66 100644
--- a/purchaseapp/entity/order.go
+++ b/purchaseapp/entity/order.go
@@ -1,4 +1,59 @@
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
+ Address string
+ CreatedAt time.Time
+ UpdatedAt time.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
+}
+
+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"
+)
+
+type PaymentStatus string
+
+const (
+ Paid PaymentStatus = "paid"
+ UnPaid = "unpaid"
+ Cancelled = "cancelled"
+)
diff --git a/purchaseapp/repository/migrations/2026010411120_create_orders_table.sql b/purchaseapp/repository/migrations/2026010411120_create_orders_table.sql
new file mode 100644
index 00000000..2fe04c92
--- /dev/null
+++ b/purchaseapp/repository/migrations/2026010411120_create_orders_table.sql
@@ -0,0 +1,23 @@
+-- +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,
+ `user_id` INT NOT NULL,
+ `address` TEXT,
+ `shipping_id` INT NOT NULL,
+ `payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online',
+ `payment_status` ENUM('unpaid', 'paid', 'cancelled') DEFAULT 'unpaid',
+ `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_discount` INT NULL,
+
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+-- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
+-- FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`)
+
+);
+
+-- +migrate Down
+DROP TABLE `orders`;
diff --git a/purchaseapp/repository/migrations/20260104_11121_create_order_items_table.sql b/purchaseapp/repository/migrations/20260104_11121_create_order_items_table.sql
new file mode 100644
index 00000000..bd209205
--- /dev/null
+++ b/purchaseapp/repository/migrations/20260104_11121_create_order_items_table.sql
@@ -0,0 +1,19 @@
+-- +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 `order_items` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `order_id` INT NOT NULL,
+ `product_id` INT NOT NULL,
+ `quantity` INT DEFAULT 1,
+ `price` INT NOT NULL,
+ `price_with_discount` INT NULL,
+ `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`)
+
+);
+
+-- +migrate Down
+DROP TABLE `order_items`;
diff --git a/purchaseapp/repository/mysql/order.go b/purchaseapp/repository/mysql/order.go
new file mode 100644
index 00000000..279aa351
--- /dev/null
+++ b/purchaseapp/repository/mysql/order.go
@@ -0,0 +1,69 @@
+package mysql
+
+import (
+ 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"
+)
+
+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) {
+
+ const Op = "repository.mysql.order.createorder"
+ tx, err := d.conn.Conn().Begin()
+
+ if err != nil {
+ return 0, richerror.New(Op).WithErr(err)
+ }
+
+ defer tx.Rollback()
+
+ query := "insert into orders(user_id, address, shipping_id," +
+ " payment_method, payment_status, process_status," +
+ " total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);"
+ res, oErr := tx.Exec(query, order.UserID, order.Address, order.ShippingID,
+ order.PaymentMethod, order.PaymentStatus, order.ProcessStatus,
+ order.TotalAmount, order.TotalDiscount)
+
+ if oErr != nil {
+ return 0, richerror.New(Op).WithErr(oErr)
+ }
+ orderID, insertIDErr := res.LastInsertId()
+ if insertIDErr != nil {
+ return 0, richerror.New(Op).WithErr(insertIDErr)
+ }
+
+ orderItemQuery := "insert into order_items(order_id, product_id, quantity, price, price_with_discount) values(?, ?, ?, ?, ?);"
+ for _, item := range orderItems {
+ _, iErr := tx.Exec(orderItemQuery, orderID, item.ProductID, item.Quantity, item.Price, item.PriceWithDiscount)
+ if iErr != nil {
+ return 0, richerror.New(Op).WithErr(iErr)
+ }
+ }
+
+ if err := tx.Commit(); err != nil {
+ return 0, richerror.New(Op).WithErr(err)
+ }
+
+ return types.ID(orderID), nil
+}
+
+func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
+ const Op = "repository.mysql.order.update-order-process-status"
+ _, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID)
+
+ if err != nil {
+ return false, richerror.New(Op).WithErr(err)
+ }
+
+ return true, nil
+
+}
diff --git a/purchaseapp/service/invoice/param.go b/purchaseapp/service/invoice/param.go
deleted file mode 100644
index 958a6107..00000000
--- a/purchaseapp/service/invoice/param.go
+++ /dev/null
@@ -1 +0,0 @@
-package invoice
diff --git a/purchaseapp/service/invoice/service.go b/purchaseapp/service/invoice/service.go
deleted file mode 100644
index 63c4a3f5..00000000
--- a/purchaseapp/service/invoice/service.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package invoice
-
-type Service struct {
-}
diff --git a/purchaseapp/service/invoice/validator.go b/purchaseapp/service/invoice/validator.go
deleted file mode 100644
index 958a6107..00000000
--- a/purchaseapp/service/invoice/validator.go
+++ /dev/null
@@ -1 +0,0 @@
-package invoice
diff --git a/purchaseapp/service/order/param.go b/purchaseapp/service/order/param.go
index 175f0c10..d4a589f9 100644
--- a/purchaseapp/service/order/param.go
+++ b/purchaseapp/service/order/param.go
@@ -1 +1,20 @@
package order
+
+import (
+ "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
+ "git.gocasts.ir/ebhomengo/niki/types"
+)
+
+type CreateOrderRequest struct {
+ UserID types.ID `json:"user_id"`
+ Address string `json:"address"`
+ ShippingID types.ID `json:"shipping_id"`
+ PaymentMethod entity.PaymentMethod `json:"payment_method"`
+ TotalAmount types.Price `json:"total_amount"`
+ TotalDiscount types.Price `json:"total_discount"`
+ OrderItems []entity.OrderItem `json:"order_items"`
+}
+
+type CreateOrderResponse struct {
+ OrderID types.ID
+}
diff --git a/purchaseapp/service/order/service.go b/purchaseapp/service/order/service.go
index 4ddf977f..21690140 100644
--- a/purchaseapp/service/order/service.go
+++ b/purchaseapp/service/order/service.go
@@ -1,4 +1,42 @@
package order
+import (
+ richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
+ "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
+ "git.gocasts.ir/ebhomengo/niki/types"
+)
+
type Service struct {
+ repo Repo
+}
+
+type Repo interface {
+ CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error)
+ UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error)
+}
+
+func New(orderRepo Repo) Service {
+ return Service{repo: orderRepo}
+}
+
+func (s Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (CreateOrderResponse, error) {
+ const Op = "purchaseapp.service.CreateOrder"
+ orderID, err := s.repo.CreateOrder(order, orderItems)
+
+ if err != nil {
+ return CreateOrderResponse{}, richerror.New(Op).WithErr(err)
+ }
+
+ return CreateOrderResponse{OrderID: orderID}, nil
+}
+
+func (s Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
+
+ const Op = "purchaseapp.service.UpdateOrderProcessStatus"
+ _, err := s.repo.UpdateOrderProcessStatus(orderID, status)
+ if err != nil {
+ return false, richerror.New(Op).WithErr(err)
+ }
+
+ return true, nil
}
diff --git a/repository/mysql/db.go b/repository/mysql/db.go
index eefe5508..1c8b08a1 100644
--- a/repository/mysql/db.go
+++ b/repository/mysql/db.go
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
+ _ "github.com/go-sql-driver/mysql"
"sync"
"time"
diff --git a/staffapp/cmd/main.go b/staffapp/cmd/main.go
deleted file mode 100644
index 0d684245..00000000
--- a/staffapp/cmd/main.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "log"
- "net/http"
-)
-
-func main() {
- fmt.Println(" Staffapp Server Starting...")
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Staffapp OK!")
- })
- log.Fatal(http.ListenAndServe(":8080", nil))
-}
diff --git a/staffapp/repository/database/db.go b/staffapp/repository/database/db.go
index 5d07c611..c1d2a621 100644
--- a/staffapp/repository/database/db.go
+++ b/staffapp/repository/database/db.go
@@ -1,13 +1,64 @@
package database
-import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
+import (
+ "fmt"
+
+ "git.gocasts.ir/ebhomengo/niki/staffapp/service"
+)
type DB struct {
- conn *mysql.DB
+ staffs []service.Staff
+ nextID int
}
-func New(conn *mysql.DB) *DB {
+func New() service.StaffRepository {
return &DB{
- conn: conn,
+ staffs: []service.Staff{},
+ nextID: 1,
}
}
+
+func (d *DB) Create(staff service.Staff) (service.Staff, error) {
+ staff.ID = d.nextID
+ d.staffs = append(d.staffs, staff)
+ d.nextID++
+ return staff, nil
+}
+
+func (d *DB) Get(id int) (*service.Staff, error) {
+ for _, v := range d.staffs {
+ if id == v.ID {
+ return &v, nil
+ }
+
+ }
+
+ return nil, fmt.Errorf("The user not found with id = %d", id)
+}
+
+func (d *DB) List() ([]service.Staff, error) {
+ return d.staffs, nil
+}
+func (d *DB) Remove(id int) error {
+ for _, v := range d.staffs {
+ if id == v.ID {
+ d.staffs = append(d.staffs[:id], d.staffs[id+1:]...)
+ return nil
+ }
+
+ }
+ return fmt.Errorf("User with ID: %d notfound", id)
+}
+func (d *DB) Update(id int, name, lastName, phoneNumber string) (*service.Staff, error) {
+ for _, v := range d.staffs {
+ if id == v.ID {
+ v.Name = name
+ v.LastName = lastName
+ v.PhoneNumber = phoneNumber
+
+ return &v, nil
+ }
+
+ }
+ return nil, fmt.Errorf("User with this Id(%d)couldn't found for updating", id)
+}
diff --git a/staffapp/service/entity.go b/staffapp/service/entity.go
deleted file mode 100644
index 6d43c336..00000000
--- a/staffapp/service/entity.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/staffapp/service/param.go b/staffapp/service/param.go
deleted file mode 100644
index 6d43c336..00000000
--- a/staffapp/service/param.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/staffapp/service/repository.go b/staffapp/service/repository.go
new file mode 100644
index 00000000..5781858b
--- /dev/null
+++ b/staffapp/service/repository.go
@@ -0,0 +1,15 @@
+package service
+
+type Staff struct {
+ ID int
+ Name string
+ LastName string
+ PhoneNumber string
+}
+type StaffRepository interface {
+ Create(staff Staff) (Staff, error)
+ Get(id int) (*Staff, error)
+ List() ([]Staff, error)
+ Remove(id int) error
+ Update(id int, name, lastName, phoneNumber string) (*Staff, error)
+}
diff --git a/staffapp/service/service.go b/staffapp/service/service.go
index 6d43c336..d78e7b06 100644
--- a/staffapp/service/service.go
+++ b/staffapp/service/service.go
@@ -1 +1,44 @@
package service
+
+import (
+ "fmt"
+)
+
+type StaffService struct {
+ repo StaffRepository
+}
+
+func NewStaffService(repo StaffRepository) *StaffService {
+ return &StaffService{
+ repo: repo,
+ }
+}
+
+func (s *StaffService) RegisterStaff(name, lastname, phone string) (Staff, error) {
+ newStaff := Staff{
+ Name: name,
+ LastName: lastname,
+ PhoneNumber: phone,
+ }
+ createdStaff, err := s.repo.Create(newStaff)
+ if err != nil {
+ return Staff{}, fmt.Errorf("Error in registring staff%w", err)
+ }
+ return createdStaff, nil
+}
+
+func (s *StaffService) Get(id int) (*Staff, error) {
+ return s.repo.Get(id)
+}
+
+func (s *StaffService) List() ([]Staff, error) {
+ return s.repo.List()
+}
+
+func (s *StaffService) Remove(id int) error {
+ return s.repo.Remove(id)
+}
+
+func (s *StaffService) Update(id int, name, lastName, phoneNumber string) (*Staff, error) {
+ return s.repo.Update(id, name, lastName, phoneNumber)
+}
diff --git a/staffapp/service/validator.go b/staffapp/service/validator.go
deleted file mode 100644
index 6d43c336..00000000
--- a/staffapp/service/validator.go
+++ /dev/null
@@ -1 +0,0 @@
-package service
diff --git a/types/count.go b/types/count.go
new file mode 100644
index 00000000..ff20ea41
--- /dev/null
+++ b/types/count.go
@@ -0,0 +1,3 @@
+package types
+
+type Count uint32
diff --git a/types/id.go b/types/id.go
new file mode 100644
index 00000000..fb0c5a25
--- /dev/null
+++ b/types/id.go
@@ -0,0 +1,3 @@
+package types
+
+type ID uint64
diff --git a/types/price.go b/types/price.go
new file mode 100644
index 00000000..195869d4
--- /dev/null
+++ b/types/price.go
@@ -0,0 +1,3 @@
+package types
+
+type Price uint64
diff --git a/vendor/dario.cat/mergo/.gitignore b/vendor/dario.cat/mergo/.gitignore
index 529c3412..45ad0f1a 100644
--- a/vendor/dario.cat/mergo/.gitignore
+++ b/vendor/dario.cat/mergo/.gitignore
@@ -13,6 +13,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
+# Golang/Intellij
+.idea
+
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
diff --git a/vendor/dario.cat/mergo/FUNDING.json b/vendor/dario.cat/mergo/FUNDING.json
new file mode 100644
index 00000000..0585e1fe
--- /dev/null
+++ b/vendor/dario.cat/mergo/FUNDING.json
@@ -0,0 +1,7 @@
+{
+ "drips": {
+ "ethereum": {
+ "ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930"
+ }
+ }
+}
diff --git a/vendor/dario.cat/mergo/README.md b/vendor/dario.cat/mergo/README.md
index 7d0cf9f3..0e4a59af 100644
--- a/vendor/dario.cat/mergo/README.md
+++ b/vendor/dario.cat/mergo/README.md
@@ -44,13 +44,21 @@ Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the
## Status
-It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, Microsoft, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
+Mergo is stable and frozen, ready for production. Check a short list of the projects using at large scale it [here](https://github.com/imdario/mergo#mergo-in-the-wild).
+
+No new features are accepted. They will be considered for a future v2 that improves the implementation and fixes bugs for corner cases.
### Important notes
#### 1.0.0
-In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`.
+In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`. No more v1 versions will be released.
+
+If the vanity URL is causing issues in your project due to a dependency pulling Mergo - it isn't a direct dependency in your project - it is recommended to use [replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to pin the version to the last one with the old import URL:
+
+```
+replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
+```
#### 0.3.9
@@ -64,55 +72,23 @@ If you were using Mergo before April 6th, 2015, please check your project works
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
-
### Mergo in the wild
-- [moby/moby](https://github.com/moby/moby)
-- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
-- [vmware/dispatch](https://github.com/vmware/dispatch)
-- [Shopify/themekit](https://github.com/Shopify/themekit)
-- [imdario/zas](https://github.com/imdario/zas)
-- [matcornic/hermes](https://github.com/matcornic/hermes)
-- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
-- [kataras/iris](https://github.com/kataras/iris)
-- [michaelsauter/crane](https://github.com/michaelsauter/crane)
-- [go-task/task](https://github.com/go-task/task)
-- [sensu/uchiwa](https://github.com/sensu/uchiwa)
-- [ory/hydra](https://github.com/ory/hydra)
-- [sisatech/vcli](https://github.com/sisatech/vcli)
-- [dairycart/dairycart](https://github.com/dairycart/dairycart)
-- [projectcalico/felix](https://github.com/projectcalico/felix)
-- [resin-os/balena](https://github.com/resin-os/balena)
-- [go-kivik/kivik](https://github.com/go-kivik/kivik)
-- [Telefonica/govice](https://github.com/Telefonica/govice)
-- [supergiant/supergiant](supergiant/supergiant)
-- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
-- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
-- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
-- [EagerIO/Stout](https://github.com/EagerIO/Stout)
-- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
-- [russross/canvasassignments](https://github.com/russross/canvasassignments)
-- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
-- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
-- [divshot/gitling](https://github.com/divshot/gitling)
-- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
-- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
-- [elwinar/rambler](https://github.com/elwinar/rambler)
-- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
-- [jfbus/impressionist](https://github.com/jfbus/impressionist)
-- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
-- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
-- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
-- [thoas/picfit](https://github.com/thoas/picfit)
-- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
-- [jnuthong/item_search](https://github.com/jnuthong/item_search)
-- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
-- [containerssh/containerssh](https://github.com/containerssh/containerssh)
-- [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
-- [tjpnz/structbot](https://github.com/tjpnz/structbot)
+Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/dependents) [of](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.16/dependents) [projects](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.12), including:
+
+* [containerd/containerd](https://github.com/containerd/containerd)
+* [datadog/datadog-agent](https://github.com/datadog/datadog-agent)
+* [docker/cli/](https://github.com/docker/cli/)
+* [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
+* [go-micro/go-micro](https://github.com/go-micro/go-micro)
+* [grafana/loki](https://github.com/grafana/loki)
+* [masterminds/sprig](github.com/Masterminds/sprig)
+* [moby/moby](https://github.com/moby/moby)
+* [slackhq/nebula](https://github.com/slackhq/nebula)
+* [volcano-sh/volcano](https://github.com/volcano-sh/volcano)
## Install
@@ -141,6 +117,39 @@ if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
}
```
+If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use `WithoutDereference`:
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "dario.cat/mergo"
+)
+
+type Foo struct {
+ A *string
+ B int64
+}
+
+func main() {
+ first := "first"
+ second := "second"
+ src := Foo{
+ A: &first,
+ B: 2,
+ }
+
+ dest := Foo{
+ A: &second,
+ B: 1,
+ }
+
+ mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference)
+}
+```
+
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
```go
@@ -181,10 +190,6 @@ func main() {
}
```
-Note: if test are failing due missing package, please execute:
-
- go get gopkg.in/yaml.v3
-
### Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
diff --git a/vendor/dario.cat/mergo/SECURITY.md b/vendor/dario.cat/mergo/SECURITY.md
index a5de61f7..3788fcc1 100644
--- a/vendor/dario.cat/mergo/SECURITY.md
+++ b/vendor/dario.cat/mergo/SECURITY.md
@@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
-| 0.3.x | :white_check_mark: |
-| < 0.3 | :x: |
+| 1.x.x | :white_check_mark: |
+| < 1.0 | :x: |
## Security contact information
diff --git a/vendor/dario.cat/mergo/map.go b/vendor/dario.cat/mergo/map.go
index b50d5c2a..759b4f74 100644
--- a/vendor/dario.cat/mergo/map.go
+++ b/vendor/dario.cat/mergo/map.go
@@ -58,7 +58,7 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, conf
}
fieldName := field.Name
fieldName = changeInitialCase(fieldName, unicode.ToLower)
- if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v), !config.ShouldNotDereference) || overwrite) {
+ if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue {
dstMap[fieldName] = src.Field(i).Interface()
}
}
diff --git a/vendor/dario.cat/mergo/merge.go b/vendor/dario.cat/mergo/merge.go
index 0ef9b213..fd47c95b 100644
--- a/vendor/dario.cat/mergo/merge.go
+++ b/vendor/dario.cat/mergo/merge.go
@@ -269,7 +269,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
- } else {
+ } else if src.Elem().Kind() != reflect.Struct {
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
dst.Set(src)
}
diff --git a/vendor/github.com/docker/docker/AUTHORS b/vendor/github.com/docker/docker/AUTHORS
index 5f93eeb4..c7c64947 100644
--- a/vendor/github.com/docker/docker/AUTHORS
+++ b/vendor/github.com/docker/docker/AUTHORS
@@ -2,7 +2,10 @@
# This file lists all contributors to the repository.
# See hack/generate-authors.sh to make modifications.
+17neverends
+7sunarni <710720732@qq.com>
Aanand Prasad
+Aarni Koskela
Aaron Davidson
Aaron Feng
Aaron Hnatiw
@@ -11,6 +14,7 @@ Aaron L. Xu
Aaron Lehmann
Aaron Welch
Aaron Yoshitake
+Abdur Rehman
Abel Muiño
Abhijeet Kasurde
Abhinandan Prativadi
@@ -24,9 +28,11 @@ Adam Avilla
Adam Dobrawy
Adam Eijdenberg
Adam Kunk
+Adam Lamers
Adam Miller
Adam Mills
Adam Pointer
+Adam Simon
Adam Singer
Adam Thornton
Adam Walz
@@ -119,6 +125,7 @@ amangoel
Amen Belayneh
Ameya Gawde
Amir Goldstein
+AmirBuddy
Amit Bakshi
Amit Krishnan
Amit Shukla
@@ -168,6 +175,7 @@ Andrey Kolomentsev
Andrey Petrov
Andrey Stolbovsky
André Martins
+Andrés Maldonado
Andy Chambers
andy diller
Andy Goldstein
@@ -182,6 +190,7 @@ Anes Hasicic
Angel Velazquez
Anil Belur
Anil Madhavapeddy
+Anirudh Aithal
Ankit Jain
Ankush Agarwal
Anonmily
@@ -219,7 +228,8 @@ Artur Meyster
Arun Gupta
Asad Saeeduddin
Asbjørn Enge
-Austin Vazquez
+Ashly Mathew
+Austin Vazquez
averagehuman
Avi Das
Avi Kivity
@@ -285,6 +295,7 @@ Brandon Liu
Brandon Philips
Brandon Rhodes
Brendan Dixon
+Brendon Smith
Brennan Kinney <5098581+polarathene@users.noreply.github.com>
Brent Salisbury
Brett Higgins
@@ -339,12 +350,14 @@ Casey Bisson
Catalin Pirvu
Ce Gao
Cedric Davies
+Cesar Talledo
Cezar Sa Espinola
Chad Swenson
Chance Zibolski
Chander Govindarajan
Chanhun Jeong
Chao Wang
+Charity Kathure
Charles Chan
Charles Hooper
Charles Law
@@ -366,6 +379,7 @@ Chen Qiu
Cheng-mean Liu
Chengfei Shang
Chengguang Xu
+Chengyu Zhu
Chentianze
Chenyang Yan
chenyuzhu
@@ -480,6 +494,7 @@ Daniel Farrell
Daniel Garcia
Daniel Gasienica
Daniel Grunwell
+Daniel Guns
Daniel Helfand
Daniel Hiltgen
Daniel J Walsh
@@ -763,6 +778,7 @@ Frank Macreery
Frank Rosquin
Frank Villaro-Dixon
Frank Yang
+François Scala
Fred Lifton
Frederick F. Kautz IV
Frederico F. de Oliveira
@@ -798,6 +814,7 @@ GennadySpb
Geoff Levand
Geoffrey Bachelet
Geon Kim
+George Adams
George Kontridze
George Ma
George MacRorie
@@ -826,6 +843,7 @@ Gopikannan Venugopalsamy
Gosuke Miyashita
Gou Rao
Govinda Fichtner
+Grace Choi
Grant Millar
Grant Reaber
Graydon Hoare
@@ -966,6 +984,7 @@ James Nugent
James Sanders
James Turnbull
James Watkins-Harvey
+Jameson Hyde
Jamie Hannaford
Jamshid Afshar
Jan Breig
@@ -1064,13 +1083,16 @@ Jim Perrin
Jimmy Cuadra
Jimmy Puckett
Jimmy Song
+jinjiadu
Jinsoo Park
Jintao Zhang
Jiri Appl
Jiri Popelka
Jiuyue Ma
Jiří Župka
+jjimbo137 <115816493+jjimbo137@users.noreply.github.com>
Joakim Roubert
+Joan Grau
Joao Fernandes
Joao Trindade
Joe Beda
@@ -1155,6 +1177,7 @@ Josiah Kiehl
José Tomás Albornoz
Joyce Jang
JP
+JSchltggr
Julian Taylor
Julien Barbier
Julien Bisconti
@@ -1189,6 +1212,7 @@ K. Heller
Kai Blin
Kai Qiang Wu (Kennan)
Kaijie Chen
+Kaita Nakamura
Kamil Domański
Kamjar Gerami
Kanstantsin Shautsou
@@ -1263,6 +1287,7 @@ Krasi Georgiev
Krasimir Georgiev
Kris-Mikael Krister
Kristian Haugene
+Kristian Heljas
Kristina Zabunova
Krystian Wojcicki
Kunal Kushwaha
@@ -1289,6 +1314,7 @@ Laura Brehm
Laura Frank
Laurent Bernaille
Laurent Erignoux
+Laurent Goderre
Laurie Voss
Leandro Motta Barros
Leandro Siqueira
@@ -1369,6 +1395,7 @@ Madhan Raj Mookkandy
Madhav Puri
Madhu Venugopal
Mageee
+maggie44 <64841595+maggie44@users.noreply.github.com>
Mahesh Tiyyagura
malnick
Malte Janduda
@@ -1462,6 +1489,7 @@ Matthias Kühnle
Matthias Rampke
Matthieu Fronton
Matthieu Hauglustaine
+Matthieu MOREL
Mattias Jernberg
Mauricio Garavaglia
mauriyouth
@@ -1579,6 +1607,7 @@ Muayyad Alsadi
Muhammad Zohaib Aslam
Mustafa Akın
Muthukumar R
+Myeongjoon Kim
Máximo Cuadros
Médi-Rémi Hashim
Nace Oroz
@@ -1593,6 +1622,7 @@ Natasha Jarus
Nate Brennand
Nate Eagleson
Nate Jones
+Nathan Baulch
Nathan Carlson
Nathan Herald
Nathan Hsieh
@@ -1655,6 +1685,7 @@ Nuutti Kotivuori
nzwsch
O.S. Tezer
objectified
+Octol1ttle
Odin Ugedal
Oguz Bilgic
Oh Jinkyun
@@ -1689,6 +1720,7 @@ Patrick Hemmer
Patrick St. laurent
Patrick Stapleton
Patrik Cyvoct
+Patrik Leifert
pattichen
Paul "TBBle" Hampson
Paul
@@ -1763,6 +1795,7 @@ Pierre Carrier
Pierre Dal-Pra
Pierre Wacrenier
Pierre-Alain RIVIERE
+pinglanlu
Piotr Bogdan
Piotr Karbowski
Porjo
@@ -1790,6 +1823,7 @@ Quentin Tayssier
r0n22
Rachit Sharma
Radostin Stoyanov
+Rafael Fernández López
Rafal Jeczalik
Rafe Colton
Raghavendra K T
@@ -1845,6 +1879,7 @@ Robert Obryk
Robert Schneider
Robert Shade
Robert Stern
+Robert Sturla
Robert Terhaar
Robert Wallis
Robert Wang
@@ -1856,7 +1891,7 @@ Robin Speekenbrink
Robin Thoni
robpc
Rodolfo Carvalho
-Rodrigo Campos
+Rodrigo Campos