├── internal ├── application │ ├── payment │ │ ├── worker │ │ │ ├── tasks.go │ │ │ └── handler.go │ │ ├── module.go │ │ ├── entity │ │ │ └── payment.entity.go │ │ ├── dto │ │ │ └── payment.dto.go │ │ ├── repository │ │ │ ├── payment.repo.go │ │ │ └── payment.repo_test.go │ │ ├── service │ │ │ └── payment.service.go │ │ └── handler │ │ │ ├── payment.grpc.handler.go │ │ │ └── payment.handler.go │ └── user │ │ ├── entity │ │ └── user.entity.go │ │ ├── module.go │ │ ├── dto │ │ └── user.dto.go │ │ ├── repository │ │ ├── user.repo.go │ │ └── user.repo_test.go │ │ ├── handler │ │ ├── user.grpc.handler.go │ │ ├── user.handler.go │ │ └── user.handler_test.go │ │ └── service │ │ └── user.service.go ├── server │ ├── migration │ │ ├── providers.go │ │ └── module.go │ ├── api │ │ ├── providers.go │ │ └── module.go │ ├── worker │ │ ├── providers.go │ │ └── module.go │ └── grpc │ │ ├── providers.go │ │ └── module.go ├── pkg │ ├── testutil │ │ ├── logger.go │ │ ├── database.go │ │ ├── fixtures.go │ │ └── mocks.go │ ├── queue │ │ ├── logger.go │ │ ├── client.go │ │ └── server.go │ ├── logger │ │ └── logger.go │ └── database │ │ └── database.go ├── middleware │ └── middleware.go └── config │ └── config.go ├── config.sample.yaml ├── Dockerfile ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── scripts └── install-pre-commit.sh ├── cmd ├── worker │ └── main.go ├── grpc │ └── main.go ├── api │ ├── main.go │ └── server.go └── migration │ └── main.go ├── api └── proto │ ├── user │ ├── user.proto │ └── user_grpc.pb.go │ └── payment │ ├── payment.proto │ └── payment_grpc.pb.go ├── .golangci.yml ├── go.mod ├── test └── integration │ └── user_test.go ├── Makefile └── TODO.md /internal/application/payment/worker/tasks.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | const ( 4 | TypeCheckPaymentStatus = "payment:check_status" 5 | TypeProcessPayment = "payment:process" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/server/migration/providers.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "go.uber.org/fx" 5 | ) 6 | 7 | var Module = fx.Options( 8 | fx.Provide(NewServer), 9 | ) 10 | -------------------------------------------------------------------------------- /internal/server/api/providers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment" 5 | "vibe-ddd-golang/internal/application/user" 6 | 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Options( 11 | // Include all domain modules 12 | user.Module, 13 | payment.Module, 14 | 15 | // API api 16 | fx.Provide(NewServer), 17 | ) 18 | -------------------------------------------------------------------------------- /internal/server/worker/providers.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment" 5 | "vibe-ddd-golang/internal/application/user" 6 | 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Options( 11 | // Include domain worker modules 12 | payment.WorkerModule, 13 | user.WorkerModule, 14 | 15 | // Worker api 16 | fx.Provide(NewServer), 17 | ) 18 | -------------------------------------------------------------------------------- /internal/pkg/testutil/logger.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zaptest" 8 | ) 9 | 10 | // NewTestLogger creates a test logger that outputs to the test 11 | func NewTestLogger(t *testing.T) *zap.Logger { 12 | return zaptest.NewLogger(t, zaptest.Level(zap.DebugLevel)) 13 | } 14 | 15 | // NewSilentLogger creates a logger that discards all output 16 | func NewSilentLogger() *zap.Logger { 17 | return zap.NewNop() 18 | } 19 | -------------------------------------------------------------------------------- /config.sample.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | host: localhost 3 | port: 8080 4 | read_timeout: 10s 5 | write_timeout: 10s 6 | idle_timeout: 60s 7 | 8 | database: 9 | host: localhost 10 | port: 5432 11 | user: postgres 12 | password: postgres 13 | db_name: vibe_db 14 | ssl_mode: disable 15 | 16 | redis: 17 | host: localhost 18 | port: 6379 19 | password: "" 20 | db: 0 21 | 22 | worker: 23 | concurrency: 10 24 | payment_check_interval: 5m 25 | retry_max_attempts: 3 26 | retry_delay: 30s 27 | 28 | logger: 29 | level: info 30 | format: json 31 | output_path: stdout -------------------------------------------------------------------------------- /internal/server/grpc/providers.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment" 5 | paymentHandler "vibe-ddd-golang/internal/application/payment/handler" 6 | "vibe-ddd-golang/internal/application/user" 7 | userHandler "vibe-ddd-golang/internal/application/user/handler" 8 | 9 | "go.uber.org/fx" 10 | ) 11 | 12 | var Module = fx.Options( 13 | // Include domain modules 14 | user.Module, 15 | payment.Module, 16 | 17 | // gRPC handlers 18 | fx.Provide( 19 | userHandler.NewUserGrpcHandler, 20 | paymentHandler.NewPaymentGrpcHandler, 21 | NewServer, 22 | ), 23 | ) 24 | -------------------------------------------------------------------------------- /internal/application/user/entity/user.entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type User struct { 10 | ID uint `json:"id" gorm:"primaryKey"` 11 | Name string `json:"name" gorm:"not null"` 12 | Email string `json:"email" gorm:"uniqueIndex;not null"` 13 | Password string `json:"-" gorm:"not null"` 14 | CreatedAt time.Time `json:"created_at"` 15 | UpdatedAt time.Time `json:"updated_at"` 16 | DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"` 17 | } 18 | 19 | func (u User) TableName() string { 20 | return "users" 21 | } 22 | -------------------------------------------------------------------------------- /internal/application/user/module.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/user/handler" 5 | "vibe-ddd-golang/internal/application/user/repository" 6 | "vibe-ddd-golang/internal/application/user/service" 7 | 8 | "go.uber.org/fx" 9 | ) 10 | 11 | // Module provides all user domain dependencies 12 | var Module = fx.Options( 13 | fx.Provide( 14 | repository.NewUserRepository, 15 | service.NewUserService, 16 | handler.NewUserHandler, 17 | ), 18 | ) 19 | 20 | // WorkerModule provides only worker dependencies for worker api 21 | var WorkerModule = fx.Options( 22 | fx.Provide( 23 | repository.NewUserRepository, 24 | service.NewUserService, 25 | ), 26 | ) 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.21-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Install dependencies 7 | RUN apk add --no-cache git 8 | 9 | # Copy go mod files 10 | COPY go.mod go.sum ./ 11 | RUN go mod download 12 | 13 | # Copy source code 14 | COPY . . 15 | 16 | # Build the application 17 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 18 | 19 | # Final stage 20 | FROM alpine:latest 21 | 22 | WORKDIR /root/ 23 | 24 | # Install ca-certificates for HTTPS 25 | RUN apk --no-cache add ca-certificates 26 | 27 | # Copy the binary from builder 28 | COPY --from=builder /app/main . 29 | 30 | # Copy config files if they exist 31 | COPY --from=builder /app/config.yaml* ./ 32 | 33 | # Expose port 34 | EXPOSE 8080 35 | 36 | # Run the binary 37 | CMD ["./main"] -------------------------------------------------------------------------------- /internal/pkg/queue/logger.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | type AsynqLogger struct { 8 | logger *zap.Logger 9 | } 10 | 11 | func NewAsynqLogger(logger *zap.Logger) *AsynqLogger { 12 | return &AsynqLogger{logger: logger} 13 | } 14 | 15 | func (l *AsynqLogger) Debug(args ...interface{}) { 16 | l.logger.Sugar().Debug(args...) 17 | } 18 | 19 | func (l *AsynqLogger) Info(args ...interface{}) { 20 | l.logger.Sugar().Info(args...) 21 | } 22 | 23 | func (l *AsynqLogger) Warn(args ...interface{}) { 24 | l.logger.Sugar().Warn(args...) 25 | } 26 | 27 | func (l *AsynqLogger) Error(args ...interface{}) { 28 | l.logger.Sugar().Error(args...) 29 | } 30 | 31 | func (l *AsynqLogger) Fatal(args ...interface{}) { 32 | l.logger.Sugar().Fatal(args...) 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Build output 21 | bin/ 22 | dist/ 23 | 24 | # IDE 25 | .vscode/ 26 | .idea/ 27 | *.swp 28 | *.swo 29 | *~ 30 | 31 | # OS 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Config files with sensitive data 36 | config.yaml 37 | config.local.yaml 38 | .env 39 | .env.local 40 | 41 | # Log files 42 | *.log 43 | 44 | # Coverage files 45 | coverage.out 46 | coverage.html 47 | 48 | # Docker 49 | .dockerignore 50 | 51 | # Temporary files 52 | tmp/ 53 | temp/ 54 | 55 | # AI 56 | .claude/ -------------------------------------------------------------------------------- /internal/pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/config" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | func NewLogger(cfg *config.Config) (*zap.Logger, error) { 11 | var zapConfig zap.Config 12 | 13 | if cfg.Logger.Format == "json" { 14 | zapConfig = zap.NewProductionConfig() 15 | } else { 16 | zapConfig = zap.NewDevelopmentConfig() 17 | } 18 | 19 | level, err := zapcore.ParseLevel(cfg.Logger.Level) 20 | if err != nil { 21 | return nil, err 22 | } 23 | zapConfig.Level = zap.NewAtomicLevelAt(level) 24 | 25 | if cfg.Logger.OutputPath != "" && cfg.Logger.OutputPath != "stdout" { 26 | zapConfig.OutputPaths = []string{cfg.Logger.OutputPath} 27 | } 28 | 29 | logger, err := zapConfig.Build() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return logger, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/application/payment/module.go: -------------------------------------------------------------------------------- 1 | package payment 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment/handler" 5 | "vibe-ddd-golang/internal/application/payment/repository" 6 | "vibe-ddd-golang/internal/application/payment/service" 7 | "vibe-ddd-golang/internal/application/payment/worker" 8 | "vibe-ddd-golang/internal/pkg/queue" 9 | 10 | "go.uber.org/fx" 11 | ) 12 | 13 | // Module provides all payment domain dependencies 14 | var Module = fx.Options( 15 | fx.Provide( 16 | repository.NewPaymentRepository, 17 | service.NewPaymentService, 18 | handler.NewPaymentHandler, 19 | worker.NewPaymentWorker, 20 | ), 21 | ) 22 | 23 | // WorkerModule provides only worker dependencies for worker api 24 | var WorkerModule = fx.Options( 25 | fx.Provide( 26 | repository.NewPaymentRepository, 27 | service.NewPaymentService, 28 | // Provide the queue client as AsynqClient interface 29 | func(client *queue.Client) worker.AsynqClient { 30 | return client 31 | }, 32 | worker.NewPaymentWorker, 33 | ), 34 | ) 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Pre-commit hooks configuration 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: golangci-lint 6 | name: golangci-lint 7 | entry: golangci-lint 8 | language: system 9 | args: [run, --fix] 10 | files: \.go$ 11 | pass_filenames: false 12 | 13 | - id: go-test 14 | name: Run tests 15 | entry: go 16 | language: system 17 | args: [test, -v, ./...] 18 | files: \.go$ 19 | pass_filenames: false 20 | 21 | - id: go-mod-tidy 22 | name: Go mod tidy 23 | entry: go 24 | language: system 25 | args: [mod, tidy] 26 | files: go\.(mod|sum)$ 27 | pass_filenames: false 28 | 29 | - repo: https://github.com/pre-commit/pre-commit-hooks 30 | rev: v4.4.0 31 | hooks: 32 | - id: trailing-whitespace 33 | exclude: \.pb\.go$ 34 | - id: end-of-file-fixer 35 | exclude: \.pb\.go$ 36 | - id: check-yaml 37 | - id: check-added-large-files 38 | - id: check-merge-conflict -------------------------------------------------------------------------------- /internal/pkg/testutil/database.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment/entity" 5 | userEntity "vibe-ddd-golang/internal/application/user/entity" 6 | 7 | "gorm.io/driver/sqlite" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/logger" 10 | ) 11 | 12 | // SetupTestDB creates an in-memory SQLite database for testing 13 | func SetupTestDB() (*gorm.DB, error) { 14 | db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ 15 | Logger: logger.Default.LogMode(logger.Silent), 16 | }) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // Auto-migrate all entities 22 | err = db.AutoMigrate( 23 | &userEntity.User{}, 24 | &entity.Payment{}, 25 | ) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return db, nil 31 | } 32 | 33 | // CleanDB cleans all data from test database 34 | func CleanDB(db *gorm.DB) error { 35 | // Delete in reverse order of dependencies 36 | if err := db.Exec("DELETE FROM payments").Error; err != nil { 37 | return err 38 | } 39 | if err := db.Exec("DELETE FROM users").Error; err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Rizky dwi aditya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/pkg/queue/client.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | 6 | "vibe-ddd-golang/internal/config" 7 | 8 | "github.com/hibiken/asynq" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type Client struct { 13 | client *asynq.Client 14 | logger *zap.Logger 15 | } 16 | 17 | func NewClient(cfg *config.Config, logger *zap.Logger) *Client { 18 | redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port) 19 | 20 | redisOpt := asynq.RedisClientOpt{ 21 | Addr: redisAddr, 22 | Password: cfg.Redis.Password, 23 | DB: cfg.Redis.DB, 24 | } 25 | 26 | client := asynq.NewClient(redisOpt) 27 | 28 | logger.Info("Queue client initialized", 29 | zap.String("redis_addr", redisAddr), 30 | zap.Int("redis_db", cfg.Redis.DB)) 31 | 32 | return &Client{ 33 | client: client, 34 | logger: logger, 35 | } 36 | } 37 | 38 | func (c *Client) Close() error { 39 | return c.client.Close() 40 | } 41 | 42 | func (c *Client) GetClient() *asynq.Client { 43 | return c.client 44 | } 45 | 46 | // Enqueue implements the AsynqClient interface 47 | func (c *Client) Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error) { 48 | return c.client.Enqueue(task, opts...) 49 | } 50 | -------------------------------------------------------------------------------- /internal/server/worker/module.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | paymentWorker "vibe-ddd-golang/internal/application/payment/worker" 5 | "vibe-ddd-golang/internal/pkg/queue" 6 | 7 | "github.com/hibiken/asynq" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type Server struct { 12 | paymentWorker *paymentWorker.PaymentWorker 13 | queueServer *queue.Server 14 | logger *zap.Logger 15 | } 16 | 17 | func NewServer( 18 | paymentWorker *paymentWorker.PaymentWorker, 19 | queueServer *queue.Server, 20 | logger *zap.Logger, 21 | ) *Server { 22 | return &Server{ 23 | paymentWorker: paymentWorker, 24 | queueServer: queueServer, 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (s *Server) RegisterHandlers() { 30 | s.logger.Info("Registering worker handlers") 31 | 32 | // Register payment workers 33 | s.queueServer.RegisterHandler( 34 | paymentWorker.TypeCheckPaymentStatus, 35 | asynq.HandlerFunc(s.paymentWorker.HandleCheckPaymentStatus), 36 | ) 37 | 38 | s.queueServer.RegisterHandler( 39 | paymentWorker.TypeProcessPayment, 40 | asynq.HandlerFunc(s.paymentWorker.HandleProcessPayment), 41 | ) 42 | 43 | s.logger.Info("Worker handlers registered successfully") 44 | } 45 | -------------------------------------------------------------------------------- /internal/pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "vibe-ddd-golang/internal/application/payment/entity" 7 | userEntity "vibe-ddd-golang/internal/application/user/entity" 8 | "vibe-ddd-golang/internal/config" 9 | 10 | "go.uber.org/zap" 11 | "gorm.io/driver/postgres" 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/logger" 14 | ) 15 | 16 | func NewDatabase(cfg *config.Config, log *zap.Logger) (*gorm.DB, error) { 17 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s", 18 | cfg.Database.Host, 19 | cfg.Database.User, 20 | cfg.Database.Password, 21 | cfg.Database.DBName, 22 | cfg.Database.Port, 23 | cfg.Database.SSLMode, 24 | ) 25 | 26 | gormConfig := &gorm.Config{ 27 | Logger: logger.Default.LogMode(logger.Silent), 28 | } 29 | 30 | db, err := gorm.Open(postgres.Open(dsn), gormConfig) 31 | if err != nil { 32 | log.Error("Failed to connect to database", zap.Error(err)) 33 | return nil, err 34 | } 35 | 36 | err = db.AutoMigrate( 37 | &userEntity.User{}, 38 | &entity.Payment{}, 39 | ) 40 | if err != nil { 41 | log.Error("Failed to migrate database", zap.Error(err)) 42 | return nil, err 43 | } 44 | 45 | log.Info("Database connected and migrated successfully") 46 | return db, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/application/user/dto/user.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | type CreateUserRequest struct { 6 | Name string `json:"name" binding:"required"` 7 | Email string `json:"email" binding:"required,email"` 8 | Password string `json:"password" binding:"required,min=8"` 9 | } 10 | 11 | type UpdateUserRequest struct { 12 | Name string `json:"name" binding:"required"` 13 | Email string `json:"email" binding:"required,email"` 14 | } 15 | 16 | type UpdateUserPasswordRequest struct { 17 | CurrentPassword string `json:"current_password" binding:"required"` 18 | NewPassword string `json:"new_password" binding:"required,min=8"` 19 | } 20 | 21 | type UserResponse struct { 22 | ID uint `json:"id"` 23 | Name string `json:"name"` 24 | Email string `json:"email"` 25 | CreatedAt time.Time `json:"created_at"` 26 | UpdatedAt time.Time `json:"updated_at"` 27 | } 28 | 29 | type UserListResponse struct { 30 | Data []UserResponse `json:"data"` 31 | TotalCount int64 `json:"total_count"` 32 | Page int `json:"page"` 33 | PageSize int `json:"page_size"` 34 | } 35 | 36 | type UserFilter struct { 37 | Name string `form:"name"` 38 | Email string `form:"email"` 39 | Page int `form:"page"` 40 | PageSize int `form:"page_size"` 41 | } 42 | -------------------------------------------------------------------------------- /internal/application/payment/entity/payment.entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Payment struct { 10 | ID uint `json:"id" gorm:"primaryKey"` 11 | Amount float64 `json:"amount" gorm:"not null"` 12 | Currency string `json:"currency" gorm:"size:3;not null"` 13 | Status PaymentStatus `json:"status" gorm:"default:pending"` 14 | Description string `json:"description" gorm:"size:500"` 15 | UserID uint `json:"user_id" gorm:"not null"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"` 19 | } 20 | 21 | type PaymentStatus string 22 | 23 | const ( 24 | PaymentStatusPending PaymentStatus = "pending" 25 | PaymentStatusCompleted PaymentStatus = "completed" 26 | PaymentStatusFailed PaymentStatus = "failed" 27 | PaymentStatusCanceled PaymentStatus = "canceled" 28 | ) 29 | 30 | func (p Payment) TableName() string { 31 | return "payments" 32 | } 33 | 34 | func (ps PaymentStatus) String() string { 35 | return string(ps) 36 | } 37 | 38 | func (ps PaymentStatus) IsValid() bool { 39 | switch ps { 40 | case PaymentStatusPending, PaymentStatusCompleted, PaymentStatusFailed, PaymentStatusCanceled: 41 | return true 42 | default: 43 | return false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/application/payment/dto/payment.dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type CreatePaymentRequest struct { 8 | Amount float64 `json:"amount" binding:"required,gt=0"` 9 | Currency string `json:"currency" binding:"required,len=3"` 10 | Description string `json:"description" binding:"required"` 11 | UserID uint `json:"user_id" binding:"required"` 12 | } 13 | 14 | type UpdatePaymentRequest struct { 15 | Status string `json:"status" binding:"required,oneof=pending completed failed canceled"` 16 | Description string `json:"description"` 17 | } 18 | 19 | type PaymentResponse struct { 20 | ID uint `json:"id"` 21 | Amount float64 `json:"amount"` 22 | Currency string `json:"currency"` 23 | Status string `json:"status"` 24 | Description string `json:"description"` 25 | UserID uint `json:"user_id"` 26 | CreatedAt time.Time `json:"created_at"` 27 | UpdatedAt time.Time `json:"updated_at"` 28 | } 29 | 30 | type PaymentListResponse struct { 31 | Data []PaymentResponse `json:"data"` 32 | TotalCount int64 `json:"total_count"` 33 | Page int `json:"page"` 34 | PageSize int `json:"page_size"` 35 | } 36 | 37 | type PaymentFilter struct { 38 | Status string `form:"status"` 39 | Currency string `form:"currency"` 40 | UserID uint `form:"user_id"` 41 | Page int `form:"page"` 42 | PageSize int `form:"page_size"` 43 | } 44 | -------------------------------------------------------------------------------- /scripts/install-pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install pre-commit hooks script 4 | set -e 5 | 6 | echo "Installing pre-commit hooks..." 7 | 8 | # Check if pre-commit is available 9 | if ! command -v pre-commit &> /dev/null; then 10 | echo "pre-commit not found. Installing..." 11 | 12 | # Try to install via pip 13 | if command -v pip3 &> /dev/null; then 14 | pip3 install pre-commit 15 | elif command -v pip &> /dev/null; then 16 | pip install pre-commit 17 | elif command -v brew &> /dev/null; then 18 | brew install pre-commit 19 | else 20 | echo "Error: Could not install pre-commit. Please install it manually:" 21 | echo " pip install pre-commit" 22 | echo " or visit: https://pre-commit.com/#installation" 23 | exit 1 24 | fi 25 | fi 26 | 27 | # Install the git hook scripts 28 | pre-commit install 29 | 30 | # Install required Go tools 31 | echo "Installing Go development tools..." 32 | # Install golangci-lint if not available 33 | if ! command -v golangci-lint &> /dev/null; then 34 | echo "Installing golangci-lint..." 35 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 36 | else 37 | echo "golangci-lint already installed" 38 | fi 39 | 40 | echo "Pre-commit hooks installed successfully!" 41 | echo "To run all hooks manually: pre-commit run --all-files" 42 | echo "To skip hooks during commit: git commit --no-verify" -------------------------------------------------------------------------------- /internal/server/migration/module.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment/entity" 5 | userEntity "vibe-ddd-golang/internal/application/user/entity" 6 | 7 | "go.uber.org/zap" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Server struct { 12 | db *gorm.DB 13 | logger *zap.Logger 14 | } 15 | 16 | func NewServer(db *gorm.DB, logger *zap.Logger) *Server { 17 | return &Server{ 18 | db: db, 19 | logger: logger, 20 | } 21 | } 22 | 23 | func (s *Server) RunMigrations() error { 24 | s.logger.Info("Starting database migrations") 25 | 26 | // Run auto migrations for all entities 27 | err := s.db.AutoMigrate( 28 | &userEntity.User{}, 29 | &entity.Payment{}, 30 | ) 31 | if err != nil { 32 | s.logger.Error("Failed to run database migrations", zap.Error(err)) 33 | return err 34 | } 35 | 36 | s.logger.Info("Database migrations completed successfully") 37 | return nil 38 | } 39 | 40 | func (s *Server) SeedData() error { 41 | s.logger.Info("Starting data seeding") 42 | 43 | // Add any initial data seeding here 44 | // Example: Create default admin user, initial payment statuses, etc. 45 | 46 | s.logger.Info("Data seeding completed successfully") 47 | return nil 48 | } 49 | 50 | func (s *Server) DropTables() error { 51 | s.logger.Warn("Dropping all database tables") 52 | 53 | err := s.db.Migrator().DropTable( 54 | &userEntity.User{}, 55 | &entity.Payment{}, 56 | ) 57 | if err != nil { 58 | s.logger.Error("Failed to drop database tables", zap.Error(err)) 59 | return err 60 | } 61 | 62 | s.logger.Info("Database tables dropped successfully") 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /cmd/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "vibe-ddd-golang/internal/config" 11 | "vibe-ddd-golang/internal/pkg/database" 12 | "vibe-ddd-golang/internal/pkg/logger" 13 | "vibe-ddd-golang/internal/pkg/queue" 14 | "vibe-ddd-golang/internal/server/worker" 15 | 16 | "go.uber.org/fx" 17 | ) 18 | 19 | func main() { 20 | app := fx.New( 21 | fx.Provide( 22 | config.NewConfig, 23 | logger.NewLogger, 24 | database.NewDatabase, 25 | queue.NewClient, 26 | queue.NewServer, 27 | ), 28 | worker.Module, 29 | fx.Invoke(runWorker), 30 | fx.StartTimeout(config.DefaultStartTimeout), 31 | fx.StopTimeout(config.DefaultStopTimeout), 32 | ) 33 | 34 | ctx := context.Background() 35 | if err := app.Start(ctx); err != nil { 36 | fmt.Fprintf(os.Stderr, "Failed to start worker application: %v\n", err) 37 | os.Exit(1) 38 | } 39 | 40 | // Setup graceful shutdown 41 | sigChan := make(chan os.Signal, 1) 42 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 43 | 44 | // Wait for shutdown signal 45 | <-sigChan 46 | fmt.Println("\nReceived shutdown signal, stopping worker gracefully...") 47 | 48 | if err := app.Stop(ctx); err != nil { 49 | fmt.Fprintf(os.Stderr, "Failed to stop worker application gracefully: %v\n", err) 50 | os.Exit(1) 51 | } 52 | 53 | fmt.Println("Worker stopped successfully") 54 | } 55 | 56 | func runWorker(lifecycle fx.Lifecycle, workerServer *worker.Server, queueServer *queue.Server) { 57 | // Register worker handlers 58 | workerServer.RegisterHandlers() 59 | 60 | // Start the queue api (it manages its own lifecycle) 61 | queueServer.Start(lifecycle) 62 | } 63 | -------------------------------------------------------------------------------- /internal/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | func Logger(logger *zap.Logger) gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | start := time.Now() 14 | path := c.Request.URL.Path 15 | raw := c.Request.URL.RawQuery 16 | 17 | c.Next() 18 | 19 | latency := time.Since(start) 20 | clientIP := c.ClientIP() 21 | method := c.Request.Method 22 | statusCode := c.Writer.Status() 23 | 24 | if raw != "" { 25 | path = path + "?" + raw 26 | } 27 | 28 | logger.Info("HTTP Request", 29 | zap.String("method", method), 30 | zap.String("path", path), 31 | zap.Int("status", statusCode), 32 | zap.Duration("latency", latency), 33 | zap.String("client_ip", clientIP), 34 | ) 35 | } 36 | } 37 | 38 | func Recovery(logger *zap.Logger) gin.HandlerFunc { 39 | return func(c *gin.Context) { 40 | defer func() { 41 | if err := recover(); err != nil { 42 | logger.Error("Panic recovered", 43 | zap.Any("error", err), 44 | zap.String("path", c.Request.URL.Path), 45 | zap.String("method", c.Request.Method), 46 | ) 47 | c.JSON(http.StatusInternalServerError, gin.H{ 48 | "error": "Internal domain error", 49 | }) 50 | c.Abort() 51 | } 52 | }() 53 | c.Next() 54 | } 55 | } 56 | 57 | func CORS() gin.HandlerFunc { 58 | return func(c *gin.Context) { 59 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 60 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 61 | c.Writer.Header().Set("Access-Control-Allow-Headers", 62 | "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") 63 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") 64 | 65 | if c.Request.Method == "OPTIONS" { 66 | c.AbortWithStatus(204) 67 | return 68 | } 69 | 70 | c.Next() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/grpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "vibe-ddd-golang/internal/config" 12 | "vibe-ddd-golang/internal/pkg/database" 13 | "vibe-ddd-golang/internal/pkg/logger" 14 | "vibe-ddd-golang/internal/server/grpc" 15 | 16 | "go.uber.org/fx" 17 | ) 18 | 19 | func main() { 20 | var ( 21 | port = flag.String("port", "9090", "gRPC api port") 22 | ) 23 | flag.Parse() 24 | 25 | app := fx.New( 26 | fx.Provide( 27 | config.NewConfig, 28 | logger.NewLogger, 29 | database.NewDatabase, 30 | ), 31 | grpc.Module, 32 | fx.Invoke(func(lifecycle fx.Lifecycle, grpcServer *grpc.Server) { 33 | runGRPCServer(lifecycle, grpcServer, *port) 34 | }), 35 | fx.StartTimeout(config.DefaultStartTimeout), 36 | fx.StopTimeout(config.DefaultStopTimeout), 37 | ) 38 | 39 | ctx := context.Background() 40 | if err := app.Start(ctx); err != nil { 41 | fmt.Fprintf(os.Stderr, "Failed to start gRPC application: %v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | // Setup graceful shutdown 46 | sigChan := make(chan os.Signal, 1) 47 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 48 | 49 | // Wait for shutdown signal 50 | <-sigChan 51 | fmt.Println("\nReceived shutdown signal, stopping gRPC api gracefully...") 52 | 53 | if err := app.Stop(ctx); err != nil { 54 | fmt.Fprintf(os.Stderr, "Failed to stop gRPC application gracefully: %v\n", err) 55 | os.Exit(1) 56 | } 57 | 58 | fmt.Println("gRPC api stopped successfully") 59 | } 60 | 61 | func runGRPCServer(lifecycle fx.Lifecycle, server *grpc.Server, port string) { 62 | lifecycle.Append(fx.Hook{ 63 | OnStart: func(ctx context.Context) error { 64 | go func() { 65 | if err := server.Start(port); err != nil { 66 | fmt.Fprintf(os.Stderr, "Failed to start gRPC api: %v\n", err) 67 | os.Exit(1) 68 | } 69 | }() 70 | return nil 71 | }, 72 | OnStop: func(ctx context.Context) error { 73 | server.Stop() 74 | return nil 75 | }, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /internal/pkg/testutil/fixtures.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "time" 5 | 6 | "vibe-ddd-golang/internal/application/payment/dto" 7 | "vibe-ddd-golang/internal/application/payment/entity" 8 | userDto "vibe-ddd-golang/internal/application/user/dto" 9 | userEntity "vibe-ddd-golang/internal/application/user/entity" 10 | ) 11 | 12 | // User fixtures 13 | func CreateUserFixture() *userEntity.User { 14 | return &userEntity.User{ 15 | ID: 1, 16 | Name: "John Doe", 17 | Email: "john@example.com", 18 | Password: "$2a$10$example.hashed.password", 19 | CreatedAt: time.Now(), 20 | UpdatedAt: time.Now(), 21 | } 22 | } 23 | 24 | func CreateUserRequestFixture() *userDto.CreateUserRequest { 25 | return &userDto.CreateUserRequest{ 26 | Name: "John Doe", 27 | Email: "john@example.com", 28 | Password: "password123", 29 | } 30 | } 31 | 32 | func CreateUpdateUserRequestFixture() *userDto.UpdateUserRequest { 33 | return &userDto.UpdateUserRequest{ 34 | Name: "John Updated", 35 | Email: "john.updated@example.com", 36 | } 37 | } 38 | 39 | // Payment fixtures 40 | func CreatePaymentFixture() *entity.Payment { 41 | return &entity.Payment{ 42 | ID: 1, 43 | Amount: 100.50, 44 | Currency: "USD", 45 | Status: entity.PaymentStatusPending, 46 | Description: "Test payment", 47 | UserID: 1, 48 | CreatedAt: time.Now(), 49 | UpdatedAt: time.Now(), 50 | } 51 | } 52 | 53 | func CreatePaymentRequestFixture() *dto.CreatePaymentRequest { 54 | return &dto.CreatePaymentRequest{ 55 | Amount: 100.50, 56 | Currency: "USD", 57 | Description: "Test payment", 58 | UserID: 1, 59 | } 60 | } 61 | 62 | func CreateUpdatePaymentRequestFixture() *dto.UpdatePaymentRequest { 63 | return &dto.UpdatePaymentRequest{ 64 | Status: entity.PaymentStatusCompleted.String(), 65 | Description: "Payment completed", 66 | } 67 | } 68 | 69 | func CreatePaymentFilterFixture() *dto.PaymentFilter { 70 | return &dto.PaymentFilter{ 71 | Status: "pending", 72 | Currency: "USD", 73 | UserID: 1, 74 | Page: 1, 75 | PageSize: 10, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "vibe-ddd-golang/internal/config" 11 | "vibe-ddd-golang/internal/pkg/database" 12 | "vibe-ddd-golang/internal/pkg/logger" 13 | "vibe-ddd-golang/internal/server/api" 14 | 15 | "go.uber.org/fx" 16 | ) 17 | 18 | // @title Vibe DDD Golang API 19 | // @version 1.0 20 | // @description A production-ready Go boilerplate following Domain-Driven Design (DDD) principles with NestJS-like architecture patterns. 21 | // @description Built with modern Go practices, microservice architecture, and comprehensive background job processing. 22 | // @termsOfService http://swagger.io/terms/ 23 | 24 | // @contact.name API Support 25 | // @contact.url http://www.swagger.io/support 26 | // @contact.email support@swagger.io 27 | 28 | // @license.name MIT 29 | // @license.url https://opensource.org/licenses/MIT 30 | 31 | // @host localhost:8080 32 | // @BasePath /api/v1 33 | 34 | // @securityDefinitions.basic BasicAuth 35 | 36 | // @externalDocs.description OpenAPI 37 | // @externalDocs.url https://swagger.io/resources/open-api/ 38 | 39 | func main() { 40 | app := fx.New( 41 | fx.Provide( 42 | config.NewConfig, 43 | logger.NewLogger, 44 | database.NewDatabase, 45 | ), 46 | api.Module, 47 | fx.Invoke(Run), 48 | fx.StartTimeout(config.DefaultStartTimeout), 49 | fx.StopTimeout(config.DefaultStopTimeout), 50 | ) 51 | 52 | ctx := context.Background() 53 | if err := app.Start(ctx); err != nil { 54 | fmt.Fprintf(os.Stderr, "Failed to start application: %v\n", err) 55 | os.Exit(1) 56 | } 57 | 58 | // Setup graceful shutdown 59 | sigChan := make(chan os.Signal, 1) 60 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 61 | 62 | // Wait for shutdown signal 63 | <-sigChan 64 | fmt.Println("\nReceived shutdown signal, stopping application gracefully...") 65 | 66 | if err := app.Stop(ctx); err != nil { 67 | fmt.Fprintf(os.Stderr, "Failed to stop application gracefully: %v\n", err) 68 | os.Exit(1) 69 | } 70 | 71 | fmt.Println("Application stopped successfully") 72 | } 73 | -------------------------------------------------------------------------------- /internal/pkg/queue/server.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "vibe-ddd-golang/internal/config" 8 | 9 | "github.com/hibiken/asynq" 10 | "go.uber.org/fx" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type Server struct { 15 | server *asynq.Server 16 | mux *asynq.ServeMux 17 | logger *zap.Logger 18 | cfg *config.Config 19 | } 20 | 21 | func NewServer(cfg *config.Config, logger *zap.Logger) *Server { 22 | redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port) 23 | 24 | redisOpt := asynq.RedisClientOpt{ 25 | Addr: redisAddr, 26 | Password: cfg.Redis.Password, 27 | DB: cfg.Redis.DB, 28 | } 29 | 30 | serverConfig := asynq.Config{ 31 | Concurrency: cfg.Worker.Concurrency, 32 | Queues: map[string]int{ 33 | "critical": 6, 34 | "default": 3, 35 | "low": 1, 36 | }, 37 | ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) { 38 | logger.Error("Task processing failed", 39 | zap.String("task_type", task.Type()), 40 | zap.ByteString("payload", task.Payload()), 41 | zap.Error(err)) 42 | }), 43 | Logger: NewAsynqLogger(logger), 44 | } 45 | 46 | server := asynq.NewServer(redisOpt, serverConfig) 47 | mux := asynq.NewServeMux() 48 | 49 | logger.Info("Queue api initialized", 50 | zap.String("redis_addr", redisAddr), 51 | zap.Int("concurrency", cfg.Worker.Concurrency)) 52 | 53 | return &Server{ 54 | server: server, 55 | mux: mux, 56 | logger: logger, 57 | cfg: cfg, 58 | } 59 | } 60 | 61 | func (s *Server) RegisterHandler(pattern string, handler asynq.Handler) { 62 | s.mux.Handle(pattern, handler) 63 | } 64 | 65 | func (s *Server) Start(lifecycle fx.Lifecycle) { 66 | lifecycle.Append(fx.Hook{ 67 | OnStart: func(ctx context.Context) error { 68 | go func() { 69 | s.logger.Info("Starting queue api") 70 | if err := s.server.Run(s.mux); err != nil { 71 | s.logger.Fatal("Queue api failed", zap.Error(err)) 72 | } 73 | }() 74 | return nil 75 | }, 76 | OnStop: func(ctx context.Context) error { 77 | s.logger.Info("Stopping queue api") 78 | s.server.Shutdown() 79 | return nil 80 | }, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /cmd/migration/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "vibe-ddd-golang/internal/config" 12 | "vibe-ddd-golang/internal/pkg/database" 13 | "vibe-ddd-golang/internal/pkg/logger" 14 | "vibe-ddd-golang/internal/server/migration" 15 | 16 | "go.uber.org/fx" 17 | ) 18 | 19 | func main() { 20 | var ( 21 | action = flag.String("action", "migrate", "Action to perform: migrate, seed, drop") 22 | ) 23 | flag.Parse() 24 | 25 | // Setup graceful shutdown for long-running operations 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | sigChan := make(chan os.Signal, 1) 28 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 29 | 30 | go func() { 31 | <-sigChan 32 | fmt.Println("\nReceived shutdown signal, canceling migration...") 33 | cancel() 34 | }() 35 | 36 | app := fx.New( 37 | fx.Provide( 38 | config.NewConfig, 39 | logger.NewLogger, 40 | database.NewDatabase, 41 | ), 42 | migration.Module, 43 | fx.Invoke(func(migrationServer *migration.Server) { 44 | runMigration(ctx, migrationServer, *action) 45 | }), 46 | ) 47 | 48 | if err := app.Start(ctx); err != nil { 49 | fmt.Fprintf(os.Stderr, "Failed to start migration application: %v\n", err) 50 | os.Exit(1) 51 | } 52 | 53 | if err := app.Stop(ctx); err != nil { 54 | fmt.Fprintf(os.Stderr, "Failed to stop migration application gracefully: %v\n", err) 55 | os.Exit(1) 56 | } 57 | } 58 | 59 | func runMigration(ctx context.Context, server *migration.Server, action string) { 60 | var err error 61 | 62 | switch action { 63 | case "migrate": 64 | fmt.Println("Running database migrations...") 65 | err = server.RunMigrations() 66 | case "seed": 67 | fmt.Println("Seeding database...") 68 | err = server.SeedData() 69 | case "drop": 70 | fmt.Println("Dropping database tables...") 71 | err = server.DropTables() 72 | default: 73 | fmt.Fprintf(os.Stderr, "Unknown action: %s. Available actions: migrate, seed, drop\n", action) 74 | os.Exit(1) 75 | } 76 | 77 | // Check if context was canceled 78 | select { 79 | case <-ctx.Done(): 80 | fmt.Println("Migration canceled by user") 81 | os.Exit(1) 82 | default: 83 | } 84 | 85 | if err != nil { 86 | fmt.Fprintf(os.Stderr, "Migration failed: %v\n", err) 87 | os.Exit(1) 88 | } 89 | 90 | fmt.Printf("Migration action '%s' completed successfully\n", action) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/api/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "vibe-ddd-golang/internal/config" 13 | "vibe-ddd-golang/internal/server/api" 14 | 15 | "github.com/gin-gonic/gin" 16 | "go.uber.org/fx" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | type Server struct { 21 | router *gin.Engine 22 | server *http.Server 23 | logger *zap.Logger 24 | config *config.Config 25 | apiServer *api.Server 26 | } 27 | 28 | func NewServer(cfg *config.Config, logger *zap.Logger, apiServer *api.Server) *Server { 29 | if cfg.Logger.Format == "json" { 30 | gin.SetMode(gin.ReleaseMode) 31 | } 32 | 33 | router := gin.New() 34 | 35 | server := &http.Server{ 36 | Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port), 37 | Handler: router, 38 | ReadTimeout: cfg.Server.ReadTimeout, 39 | WriteTimeout: cfg.Server.WriteTimeout, 40 | IdleTimeout: cfg.Server.IdleTimeout, 41 | } 42 | 43 | return &Server{ 44 | router: router, 45 | server: server, 46 | logger: logger, 47 | config: cfg, 48 | apiServer: apiServer, 49 | } 50 | } 51 | 52 | func (s *Server) setupRoutes() { 53 | s.apiServer.SetupRoutes(s.router) 54 | } 55 | 56 | func Run(lifecycle fx.Lifecycle, cfg *config.Config, logger *zap.Logger, apiServer *api.Server) { 57 | server := NewServer(cfg, logger, apiServer) 58 | server.setupRoutes() 59 | 60 | lifecycle.Append(fx.Hook{ 61 | OnStart: func(ctx context.Context) error { 62 | go func() { 63 | logger.Info("Starting HTTP API api", 64 | zap.String("addr", server.server.Addr)) 65 | 66 | if err := server.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 67 | logger.Fatal("Failed to start API api", zap.Error(err)) 68 | } 69 | }() 70 | 71 | go func() { 72 | quit := make(chan os.Signal, 1) 73 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 74 | <-quit 75 | logger.Info("Shutting down API api...") 76 | 77 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 78 | defer cancel() 79 | 80 | if err := server.server.Shutdown(ctx); err != nil { 81 | logger.Fatal("Server forced to shutdown", zap.Error(err)) 82 | } 83 | }() 84 | 85 | return nil 86 | }, 87 | OnStop: func(ctx context.Context) error { 88 | logger.Info("Stopping HTTP API api") 89 | return server.server.Shutdown(ctx) 90 | }, 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /internal/server/grpc/module.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "vibe-ddd-golang/api/proto/payment" 8 | "vibe-ddd-golang/api/proto/user" 9 | paymentHandler "vibe-ddd-golang/internal/application/payment/handler" 10 | userHandler "vibe-ddd-golang/internal/application/user/handler" 11 | 12 | "go.uber.org/zap" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | type Server struct { 17 | server *grpc.Server 18 | logger *zap.Logger 19 | userHandler *userHandler.UserGrpcHandler 20 | paymentHandler *paymentHandler.PaymentGrpcHandler 21 | } 22 | 23 | func NewServer( 24 | logger *zap.Logger, 25 | userHandler *userHandler.UserGrpcHandler, 26 | paymentHandler *paymentHandler.PaymentGrpcHandler, 27 | ) *Server { 28 | // Create gRPC api with options 29 | server := grpc.NewServer( 30 | grpc.UnaryInterceptor(unaryLoggingInterceptor(logger)), 31 | ) 32 | 33 | return &Server{ 34 | server: server, 35 | logger: logger, 36 | userHandler: userHandler, 37 | paymentHandler: paymentHandler, 38 | } 39 | } 40 | 41 | func (s *Server) RegisterServices() { 42 | s.logger.Info("Registering gRPC services") 43 | 44 | // Register user service 45 | user.RegisterUserServiceServer(s.server, s.userHandler) 46 | s.logger.Info("User service registered") 47 | 48 | // Register payment service 49 | payment.RegisterPaymentServiceServer(s.server, s.paymentHandler) 50 | s.logger.Info("Payment service registered") 51 | 52 | s.logger.Info("gRPC services registered successfully") 53 | } 54 | 55 | func (s *Server) Start(port string) error { 56 | s.logger.Info("Starting gRPC api", zap.String("port", port)) 57 | 58 | listener, err := net.Listen("tcp", ":"+port) 59 | if err != nil { 60 | s.logger.Error("Failed to listen on port", zap.String("port", port), zap.Error(err)) 61 | return err 62 | } 63 | 64 | s.RegisterServices() 65 | 66 | s.logger.Info("gRPC api listening", zap.String("address", listener.Addr().String())) 67 | return s.server.Serve(listener) 68 | } 69 | 70 | func (s *Server) Stop() { 71 | s.logger.Info("Stopping gRPC api") 72 | s.server.GracefulStop() 73 | } 74 | 75 | // unaryLoggingInterceptor logs gRPC calls 76 | func unaryLoggingInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor { 77 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 78 | logger.Info("gRPC call", zap.String("method", info.FullMethod)) 79 | return handler(ctx, req) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /api/proto/user/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | option go_package = "vibe-ddd-golang/api/proto/user"; 8 | 9 | // User service definition 10 | service UserService { 11 | // Create a new user 12 | rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); 13 | 14 | // Get a user by ID 15 | rpc GetUser(GetUserRequest) returns (GetUserResponse); 16 | 17 | // List users with pagination 18 | rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); 19 | 20 | // Update a user 21 | rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse); 22 | 23 | // Delete a user 24 | rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse); 25 | 26 | // Update user password 27 | rpc UpdateUserPassword(UpdateUserPasswordRequest) returns (UpdateUserPasswordResponse); 28 | } 29 | 30 | // User message 31 | message User { 32 | uint32 id = 1; 33 | string name = 2; 34 | string email = 3; 35 | google.protobuf.Timestamp created_at = 4; 36 | google.protobuf.Timestamp updated_at = 5; 37 | } 38 | 39 | // Create user request 40 | message CreateUserRequest { 41 | string name = 1; 42 | string email = 2; 43 | string password = 3; 44 | } 45 | 46 | // Create user response 47 | message CreateUserResponse { 48 | User user = 1; 49 | } 50 | 51 | // Get user request 52 | message GetUserRequest { 53 | uint32 id = 1; 54 | } 55 | 56 | // Get user response 57 | message GetUserResponse { 58 | User user = 1; 59 | } 60 | 61 | // List users request 62 | message ListUsersRequest { 63 | int32 page = 1; 64 | int32 page_size = 2; 65 | } 66 | 67 | // List users response 68 | message ListUsersResponse { 69 | repeated User users = 1; 70 | int64 total = 2; 71 | int32 page = 3; 72 | int32 page_size = 4; 73 | } 74 | 75 | // Update user request 76 | message UpdateUserRequest { 77 | uint32 id = 1; 78 | string name = 2; 79 | string email = 3; 80 | } 81 | 82 | // Update user response 83 | message UpdateUserResponse { 84 | User user = 1; 85 | } 86 | 87 | // Delete user request 88 | message DeleteUserRequest { 89 | uint32 id = 1; 90 | } 91 | 92 | // Delete user response 93 | message DeleteUserResponse { 94 | bool success = 1; 95 | } 96 | 97 | // Update user password request 98 | message UpdateUserPasswordRequest { 99 | uint32 id = 1; 100 | string old_password = 2; 101 | string new_password = 3; 102 | } 103 | 104 | // Update user password response 105 | message UpdateUserPasswordResponse { 106 | bool success = 1; 107 | } -------------------------------------------------------------------------------- /internal/server/api/module.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | paymentHandler "vibe-ddd-golang/internal/application/payment/handler" 5 | userHandler "vibe-ddd-golang/internal/application/user/handler" 6 | "vibe-ddd-golang/internal/middleware" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/swaggo/files" 10 | ginSwagger "github.com/swaggo/gin-swagger" 11 | "go.uber.org/zap" 12 | 13 | _ "vibe-ddd-golang/docs" // This will be generated by swag 14 | ) 15 | 16 | type Server struct { 17 | userHandler *userHandler.UserHandler 18 | paymentHandler *paymentHandler.PaymentHandler 19 | logger *zap.Logger 20 | } 21 | 22 | func NewServer( 23 | userHandler *userHandler.UserHandler, 24 | paymentHandler *paymentHandler.PaymentHandler, 25 | logger *zap.Logger, 26 | ) *Server { 27 | return &Server{ 28 | userHandler: userHandler, 29 | paymentHandler: paymentHandler, 30 | logger: logger, 31 | } 32 | } 33 | 34 | func (s *Server) SetupRoutes(router *gin.Engine) { 35 | // Apply global middleware 36 | router.Use(middleware.Logger(s.logger)) 37 | router.Use(middleware.Recovery(s.logger)) 38 | router.Use(middleware.CORS()) 39 | 40 | // Swagger documentation routes 41 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 42 | router.GET("/docs", func(c *gin.Context) { 43 | c.Redirect(302, "/swagger/index.html") 44 | }) 45 | 46 | // Register API routes 47 | api := router.Group("/api/v1") 48 | { 49 | s.registerHealthRoutes(api) 50 | s.userHandler.RegisterRoutes(api) 51 | s.paymentHandler.RegisterRoutes(api) 52 | } 53 | } 54 | 55 | func (s *Server) registerHealthRoutes(api *gin.RouterGroup) { 56 | health := api.Group("/health") 57 | { 58 | health.GET("", s.healthCheck) 59 | health.GET("/ready", s.readinessCheck) 60 | } 61 | } 62 | 63 | // HealthCheck godoc 64 | // @Summary Show the status of server. 65 | // @Description get the status of server. 66 | // @Tags health 67 | // @Accept */* 68 | // @Produce json 69 | // @Success 200 {object} map[string]interface{} 70 | // @Router /health [get] 71 | func (s *Server) healthCheck(c *gin.Context) { 72 | c.JSON(200, gin.H{ 73 | "status": "healthy", 74 | "service": "vibe-ddd-golang-api", 75 | "version": "1.0.0", 76 | }) 77 | } 78 | 79 | // ReadinessCheck godoc 80 | // @Summary Show the readiness of server. 81 | // @Description get the readiness of server. 82 | // @Tags health 83 | // @Accept */* 84 | // @Produce json 85 | // @Success 200 {object} map[string]interface{} 86 | // @Router /health/ready [get] 87 | func (s *Server) readinessCheck(c *gin.Context) { 88 | c.JSON(200, gin.H{ 89 | "status": "ready", 90 | "checks": gin.H{ 91 | "database": "ok", 92 | "cache": "ok", 93 | }, 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /internal/application/payment/repository/payment.repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment/dto" 5 | "vibe-ddd-golang/internal/application/payment/entity" 6 | 7 | "go.uber.org/zap" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type PaymentRepository interface { 12 | Create(payment *entity.Payment) error 13 | GetByID(id uint) (*entity.Payment, error) 14 | GetAll(filter *dto.PaymentFilter) ([]entity.Payment, int64, error) 15 | Update(payment *entity.Payment) error 16 | Delete(id uint) error 17 | GetByUserID(userID uint) ([]entity.Payment, error) 18 | } 19 | 20 | type paymentRepository struct { 21 | db *gorm.DB 22 | logger *zap.Logger 23 | } 24 | 25 | func NewPaymentRepository(db *gorm.DB, logger *zap.Logger) PaymentRepository { 26 | return &paymentRepository{ 27 | db: db, 28 | logger: logger, 29 | } 30 | } 31 | 32 | func (r *paymentRepository) Create(payment *entity.Payment) error { 33 | r.logger.Info("Creating payment", zap.Uint("user_id", payment.UserID)) 34 | return r.db.Create(payment).Error 35 | } 36 | 37 | func (r *paymentRepository) GetByID(id uint) (*entity.Payment, error) { 38 | var payment entity.Payment 39 | err := r.db.First(&payment, id).Error 40 | if err != nil { 41 | r.logger.Error("Failed to get payment by ID", zap.Uint("id", id), zap.Error(err)) 42 | return nil, err 43 | } 44 | return &payment, nil 45 | } 46 | 47 | func (r *paymentRepository) GetAll(filter *dto.PaymentFilter) ([]entity.Payment, int64, error) { 48 | var payments []entity.Payment 49 | var totalCount int64 50 | 51 | query := r.db.Model(&entity.Payment{}) 52 | 53 | if filter.Status != "" { 54 | query = query.Where("status = ?", filter.Status) 55 | } 56 | if filter.Currency != "" { 57 | query = query.Where("currency = ?", filter.Currency) 58 | } 59 | if filter.UserID != 0 { 60 | query = query.Where("user_id = ?", filter.UserID) 61 | } 62 | 63 | query.Count(&totalCount) 64 | 65 | if filter.Page > 0 && filter.PageSize > 0 { 66 | offset := (filter.Page - 1) * filter.PageSize 67 | query = query.Offset(offset).Limit(filter.PageSize) 68 | } 69 | 70 | err := query.Find(&payments).Error 71 | if err != nil { 72 | r.logger.Error("Failed to get payments", zap.Error(err)) 73 | return nil, 0, err 74 | } 75 | 76 | return payments, totalCount, nil 77 | } 78 | 79 | func (r *paymentRepository) Update(payment *entity.Payment) error { 80 | r.logger.Info("Updating payment", zap.Uint("id", payment.ID)) 81 | return r.db.Save(payment).Error 82 | } 83 | 84 | func (r *paymentRepository) Delete(id uint) error { 85 | r.logger.Info("Deleting payment", zap.Uint("id", id)) 86 | return r.db.Delete(&entity.Payment{}, id).Error 87 | } 88 | 89 | func (r *paymentRepository) GetByUserID(userID uint) ([]entity.Payment, error) { 90 | var payments []entity.Payment 91 | err := r.db.Where("user_id = ?", userID).Find(&payments).Error 92 | if err != nil { 93 | r.logger.Error("Failed to get payments by user ID", zap.Uint("user_id", userID), zap.Error(err)) 94 | return nil, err 95 | } 96 | return payments, nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/application/user/repository/user.repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/user/dto" 5 | "vibe-ddd-golang/internal/application/user/entity" 6 | 7 | "go.uber.org/zap" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type UserRepository interface { 12 | Create(user *entity.User) error 13 | GetByID(id uint) (*entity.User, error) 14 | GetByEmail(email string) (*entity.User, error) 15 | GetAll(filter *dto.UserFilter) ([]entity.User, int64, error) 16 | Update(user *entity.User) error 17 | Delete(id uint) error 18 | EmailExists(email string) (bool, error) 19 | } 20 | 21 | type userRepository struct { 22 | db *gorm.DB 23 | logger *zap.Logger 24 | } 25 | 26 | func NewUserRepository(db *gorm.DB, logger *zap.Logger) UserRepository { 27 | return &userRepository{ 28 | db: db, 29 | logger: logger, 30 | } 31 | } 32 | 33 | func (r *userRepository) Create(user *entity.User) error { 34 | r.logger.Info("Creating user", zap.String("email", user.Email)) 35 | return r.db.Create(user).Error 36 | } 37 | 38 | func (r *userRepository) GetByID(id uint) (*entity.User, error) { 39 | var user entity.User 40 | err := r.db.First(&user, id).Error 41 | if err != nil { 42 | r.logger.Error("Failed to get user by ID", zap.Uint("id", id), zap.Error(err)) 43 | return nil, err 44 | } 45 | return &user, nil 46 | } 47 | 48 | func (r *userRepository) GetByEmail(email string) (*entity.User, error) { 49 | var user entity.User 50 | err := r.db.Where("email = ?", email).First(&user).Error 51 | if err != nil { 52 | r.logger.Error("Failed to get user by email", zap.String("email", email), zap.Error(err)) 53 | return nil, err 54 | } 55 | return &user, nil 56 | } 57 | 58 | func (r *userRepository) GetAll(filter *dto.UserFilter) ([]entity.User, int64, error) { 59 | var users []entity.User 60 | var totalCount int64 61 | 62 | query := r.db.Model(&entity.User{}) 63 | 64 | if filter.Name != "" { 65 | query = query.Where("name LIKE ?", "%"+filter.Name+"%") 66 | } 67 | if filter.Email != "" { 68 | query = query.Where("email LIKE ?", "%"+filter.Email+"%") 69 | } 70 | 71 | query.Count(&totalCount) 72 | 73 | if filter.Page > 0 && filter.PageSize > 0 { 74 | offset := (filter.Page - 1) * filter.PageSize 75 | query = query.Offset(offset).Limit(filter.PageSize) 76 | } 77 | 78 | err := query.Find(&users).Error 79 | if err != nil { 80 | r.logger.Error("Failed to get users", zap.Error(err)) 81 | return nil, 0, err 82 | } 83 | 84 | return users, totalCount, nil 85 | } 86 | 87 | func (r *userRepository) Update(user *entity.User) error { 88 | r.logger.Info("Updating user", zap.Uint("id", user.ID)) 89 | return r.db.Save(user).Error 90 | } 91 | 92 | func (r *userRepository) Delete(id uint) error { 93 | r.logger.Info("Deleting user", zap.Uint("id", id)) 94 | return r.db.Delete(&entity.User{}, id).Error 95 | } 96 | 97 | func (r *userRepository) EmailExists(email string) (bool, error) { 98 | var count int64 99 | err := r.db.Model(&entity.User{}).Where("email = ?", email).Count(&count).Error 100 | return count > 0, err 101 | } 102 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | tests: false # Skip test files to avoid mock issues 4 | modules-download-mode: readonly 5 | build-tags: 6 | - integration 7 | skip-dirs: 8 | - vendor 9 | - api/proto 10 | - internal/pkg/testutil 11 | 12 | linters-settings: 13 | dupl: 14 | threshold: 100 15 | funlen: 16 | lines: 80 17 | statements: 50 18 | goconst: 19 | min-len: 2 20 | min-occurrences: 3 21 | gocritic: 22 | enabled-tags: 23 | - diagnostic 24 | - style 25 | - performance 26 | disabled-checks: 27 | - dupImport 28 | - ifElseChain 29 | - octalLiteral 30 | - whyNoLint 31 | gocyclo: 32 | min-complexity: 15 33 | goimports: 34 | local-prefixes: vibe-ddd-golang 35 | lll: 36 | line-length: 120 37 | misspell: 38 | locale: US 39 | nolintlint: 40 | allow-unused: false 41 | require-explanation: true 42 | require-specific: true 43 | revive: 44 | rules: 45 | - name: exported 46 | disabled: true 47 | - name: package-comments 48 | disabled: true 49 | 50 | linters: 51 | disable-all: true 52 | enable: 53 | - gofmt 54 | - goimports 55 | - misspell 56 | - whitespace 57 | - gocyclo 58 | - funlen 59 | - lll 60 | - nilerr # Finds code that returns nil even if it checks that error is not nil 61 | - nilnil # Checks that there is no simultaneous return of nil error and invalid value 62 | 63 | issues: 64 | skip-dirs: 65 | - vendor 66 | - api/proto 67 | - internal/pkg/testutil 68 | skip-files: 69 | - ".*\\.pb\\.go$" 70 | - ".*_gen\\.go$" 71 | - ".*_test\\.go$" 72 | - ".*mock.*\\.go$" 73 | exclude-rules: 74 | # Exclude external dependencies and system Go packages 75 | - path: ".*/go/pkg/mod/.*" 76 | linters: 77 | - all 78 | 79 | # Exclude Go toolchain files 80 | - path: ".*toolchain.*" 81 | linters: 82 | - all 83 | 84 | # Exclude system packages 85 | - path: ".*/golang\\.org/.*" 86 | linters: 87 | - all 88 | 89 | # Exclude vendor files 90 | - path: vendor/ 91 | linters: 92 | - all 93 | 94 | # Exclude test files 95 | - path: _test\.go 96 | linters: 97 | - all 98 | 99 | # Exclude generated files 100 | - path: \.pb\.go$ 101 | linters: 102 | - all 103 | 104 | # Exclude testutil and mock files 105 | - path: testutil/ 106 | linters: 107 | - all 108 | 109 | - path: mock.*\.go$ 110 | linters: 111 | - all 112 | 113 | # Allow long functions in main files 114 | - path: main\.go 115 | linters: 116 | - funlen 117 | 118 | # Allow init functions in cmd packages 119 | - path: cmd/ 120 | linters: 121 | - gochecknoinits 122 | 123 | 124 | # Allow longer lines in middleware for header lists 125 | - path: internal/middleware/ 126 | linters: 127 | - lll 128 | 129 | # Allow longer lines in api setup for gRPC interceptors 130 | - path: internal/api/grpc/ 131 | linters: 132 | - lll 133 | 134 | max-issues-per-linter: 50 135 | max-same-issues: 3 -------------------------------------------------------------------------------- /api/proto/payment/payment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package payment; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | option go_package = "vibe-ddd-golang/api/proto/payment"; 8 | 9 | // Payment service definition 10 | service PaymentService { 11 | // Create a new payment 12 | rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse); 13 | 14 | // Get a payment by ID 15 | rpc GetPayment(GetPaymentRequest) returns (GetPaymentResponse); 16 | 17 | // List payments with filtering 18 | rpc ListPayments(ListPaymentsRequest) returns (ListPaymentsResponse); 19 | 20 | // Update a payment 21 | rpc UpdatePayment(UpdatePaymentRequest) returns (UpdatePaymentResponse); 22 | 23 | // Delete a payment 24 | rpc DeletePayment(DeletePaymentRequest) returns (DeletePaymentResponse); 25 | 26 | // Get payments by user ID 27 | rpc GetUserPayments(GetUserPaymentsRequest) returns (GetUserPaymentsResponse); 28 | } 29 | 30 | // Payment status enum 31 | enum PaymentStatus { 32 | PAYMENT_STATUS_UNSPECIFIED = 0; 33 | PAYMENT_STATUS_PENDING = 1; 34 | PAYMENT_STATUS_PROCESSING = 2; 35 | PAYMENT_STATUS_COMPLETED = 3; 36 | PAYMENT_STATUS_FAILED = 4; 37 | PAYMENT_STATUS_CANCELED = 5; 38 | } 39 | 40 | // Payment message 41 | message Payment { 42 | uint32 id = 1; 43 | double amount = 2; 44 | string currency = 3; 45 | string description = 4; 46 | PaymentStatus status = 5; 47 | uint32 user_id = 6; 48 | google.protobuf.Timestamp created_at = 7; 49 | google.protobuf.Timestamp updated_at = 8; 50 | } 51 | 52 | // Create payment request 53 | message CreatePaymentRequest { 54 | double amount = 1; 55 | string currency = 2; 56 | string description = 3; 57 | uint32 user_id = 4; 58 | } 59 | 60 | // Create payment response 61 | message CreatePaymentResponse { 62 | Payment payment = 1; 63 | } 64 | 65 | // Get payment request 66 | message GetPaymentRequest { 67 | uint32 id = 1; 68 | } 69 | 70 | // Get payment response 71 | message GetPaymentResponse { 72 | Payment payment = 1; 73 | } 74 | 75 | // List payments request 76 | message ListPaymentsRequest { 77 | int32 page = 1; 78 | int32 page_size = 2; 79 | PaymentStatus status = 3; 80 | uint32 user_id = 4; 81 | } 82 | 83 | // List payments response 84 | message ListPaymentsResponse { 85 | repeated Payment payments = 1; 86 | int64 total = 2; 87 | int32 page = 3; 88 | int32 page_size = 4; 89 | } 90 | 91 | // Update payment request 92 | message UpdatePaymentRequest { 93 | uint32 id = 1; 94 | double amount = 2; 95 | string currency = 3; 96 | string description = 4; 97 | PaymentStatus status = 5; 98 | } 99 | 100 | // Update payment response 101 | message UpdatePaymentResponse { 102 | Payment payment = 1; 103 | } 104 | 105 | // Delete payment request 106 | message DeletePaymentRequest { 107 | uint32 id = 1; 108 | } 109 | 110 | // Delete payment response 111 | message DeletePaymentResponse { 112 | bool success = 1; 113 | } 114 | 115 | // Get user payments request 116 | message GetUserPaymentsRequest { 117 | uint32 user_id = 1; 118 | int32 page = 2; 119 | int32 page_size = 3; 120 | } 121 | 122 | // Get user payments response 123 | message GetUserPaymentsResponse { 124 | repeated Payment payments = 1; 125 | int64 total = 2; 126 | int32 page = 3; 127 | int32 page_size = 4; 128 | } -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | const ( 10 | DefaultStartTimeout = 15 * time.Second 11 | DefaultStopTimeout = 10 * time.Second 12 | ) 13 | 14 | type Config struct { 15 | Server ServerConfig `mapstructure:"api"` 16 | Database DatabaseConfig `mapstructure:"database"` 17 | Logger LoggerConfig `mapstructure:"logger"` 18 | Redis RedisConfig `mapstructure:"redis"` 19 | Worker WorkerConfig `mapstructure:"worker"` 20 | } 21 | 22 | type ServerConfig struct { 23 | Host string `mapstructure:"host"` 24 | Port int `mapstructure:"port"` 25 | ReadTimeout time.Duration `mapstructure:"read_timeout"` 26 | WriteTimeout time.Duration `mapstructure:"write_timeout"` 27 | IdleTimeout time.Duration `mapstructure:"idle_timeout"` 28 | } 29 | 30 | type DatabaseConfig struct { 31 | Host string `mapstructure:"host"` 32 | Port int `mapstructure:"port"` 33 | User string `mapstructure:"user"` 34 | Password string `mapstructure:"password"` 35 | DBName string `mapstructure:"db_name"` 36 | SSLMode string `mapstructure:"ssl_mode"` 37 | } 38 | 39 | type LoggerConfig struct { 40 | Level string `mapstructure:"level"` 41 | Format string `mapstructure:"format"` 42 | OutputPath string `mapstructure:"output_path"` 43 | } 44 | 45 | type RedisConfig struct { 46 | Host string `mapstructure:"host"` 47 | Port int `mapstructure:"port"` 48 | Password string `mapstructure:"password"` 49 | DB int `mapstructure:"db"` 50 | } 51 | 52 | type WorkerConfig struct { 53 | Concurrency int `mapstructure:"concurrency"` 54 | PaymentCheckInterval time.Duration `mapstructure:"payment_check_interval"` 55 | RetryMaxAttempts int `mapstructure:"retry_max_attempts"` 56 | RetryDelay time.Duration `mapstructure:"retry_delay"` 57 | } 58 | 59 | func NewConfig() (*Config, error) { 60 | viper.SetConfigName("config") 61 | viper.SetConfigType("yaml") 62 | viper.AddConfigPath(".") 63 | viper.AddConfigPath("./config") 64 | 65 | viper.SetDefault("api.host", "localhost") 66 | viper.SetDefault("api.port", 8080) 67 | viper.SetDefault("api.read_timeout", "10s") 68 | viper.SetDefault("api.write_timeout", "10s") 69 | viper.SetDefault("api.idle_timeout", "60s") 70 | 71 | viper.SetDefault("database.host", "localhost") 72 | viper.SetDefault("database.port", 5432) 73 | viper.SetDefault("database.user", "postgres") 74 | viper.SetDefault("database.password", "postgres") 75 | viper.SetDefault("database.db_name", "vibe_db") 76 | viper.SetDefault("database.ssl_mode", "disable") 77 | 78 | viper.SetDefault("logger.level", "info") 79 | viper.SetDefault("logger.format", "json") 80 | viper.SetDefault("logger.output_path", "stdout") 81 | 82 | viper.SetDefault("redis.host", "localhost") 83 | viper.SetDefault("redis.port", 6379) 84 | viper.SetDefault("redis.password", "") 85 | viper.SetDefault("redis.db", 0) 86 | 87 | viper.SetDefault("worker.concurrency", 10) 88 | viper.SetDefault("worker.payment_check_interval", "5m") 89 | viper.SetDefault("worker.retry_max_attempts", 3) 90 | viper.SetDefault("worker.retry_delay", "30s") 91 | 92 | viper.AutomaticEnv() 93 | 94 | if err := viper.ReadInConfig(); err != nil { 95 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 96 | return nil, err 97 | } 98 | } 99 | 100 | var config Config 101 | if err := viper.Unmarshal(&config); err != nil { 102 | return nil, err 103 | } 104 | 105 | return &config, nil 106 | } 107 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module vibe-ddd-golang 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.10.1 9 | github.com/hibiken/asynq v0.24.1 10 | github.com/spf13/viper v1.17.0 11 | github.com/stretchr/testify v1.10.0 12 | go.uber.org/fx v1.20.0 13 | go.uber.org/zap v1.26.0 14 | golang.org/x/crypto v0.39.0 15 | google.golang.org/grpc v1.58.2 16 | google.golang.org/protobuf v1.36.6 17 | gorm.io/driver/postgres v1.5.4 18 | gorm.io/driver/sqlite v1.5.4 19 | gorm.io/gorm v1.25.5 20 | ) 21 | 22 | require ( 23 | github.com/KyleBanks/depth v1.2.1 // indirect 24 | github.com/PuerkitoBio/purell v1.2.1 // indirect 25 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 26 | github.com/bytedance/sonic v1.13.3 // indirect 27 | github.com/bytedance/sonic/loader v0.3.0 // indirect 28 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 29 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 30 | github.com/cloudwego/base64x v0.1.5 // indirect 31 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 32 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 33 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 34 | github.com/fsnotify/fsnotify v1.7.0 // indirect 35 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 36 | github.com/gin-contrib/sse v1.1.0 // indirect 37 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 38 | github.com/go-openapi/jsonreference v0.21.0 // indirect 39 | github.com/go-openapi/spec v0.21.0 // indirect 40 | github.com/go-openapi/swag v0.23.1 // indirect 41 | github.com/go-playground/locales v0.14.1 // indirect 42 | github.com/go-playground/universal-translator v0.18.1 // indirect 43 | github.com/go-playground/validator/v10 v10.27.0 // indirect 44 | github.com/goccy/go-json v0.10.5 // indirect 45 | github.com/golang/protobuf v1.5.3 // indirect 46 | github.com/google/go-cmp v0.7.0 // indirect 47 | github.com/google/uuid v1.3.0 // indirect 48 | github.com/hashicorp/hcl v1.0.0 // indirect 49 | github.com/jackc/pgpassfile v1.0.0 // indirect 50 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 51 | github.com/jackc/pgx/v5 v5.4.3 // indirect 52 | github.com/jinzhu/inflection v1.0.0 // indirect 53 | github.com/jinzhu/now v1.1.5 // indirect 54 | github.com/josharian/intern v1.0.0 // indirect 55 | github.com/json-iterator/go v1.1.12 // indirect 56 | github.com/klauspost/cpuid/v2 v2.2.11 // indirect 57 | github.com/leodido/go-urn v1.4.0 // indirect 58 | github.com/magiconair/properties v1.8.7 // indirect 59 | github.com/mailru/easyjson v0.9.0 // indirect 60 | github.com/mattn/go-isatty v0.0.20 // indirect 61 | github.com/mattn/go-sqlite3 v1.14.17 // indirect 62 | github.com/mitchellh/mapstructure v1.5.0 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 67 | github.com/redis/go-redis/v9 v9.3.0 // indirect 68 | github.com/robfig/cron/v3 v3.0.1 // indirect 69 | github.com/rogpeppe/go-internal v1.14.1 // indirect 70 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 71 | github.com/sagikazarmark/locafero v0.3.0 // indirect 72 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 73 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 74 | github.com/sourcegraph/conc v0.3.0 // indirect 75 | github.com/spf13/afero v1.10.0 // indirect 76 | github.com/spf13/cast v1.5.1 // indirect 77 | github.com/spf13/pflag v1.0.5 // indirect 78 | github.com/stretchr/objx v0.5.2 // indirect 79 | github.com/subosito/gotenv v1.6.0 // indirect 80 | github.com/swaggo/files v1.0.1 // indirect 81 | github.com/swaggo/gin-swagger v1.6.0 // indirect 82 | github.com/swaggo/swag v1.16.4 // indirect 83 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 84 | github.com/ugorji/go/codec v1.3.0 // indirect 85 | github.com/urfave/cli/v2 v2.27.7 // indirect 86 | github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect 87 | go.uber.org/atomic v1.10.0 // indirect 88 | go.uber.org/dig v1.17.0 // indirect 89 | go.uber.org/multierr v1.10.0 // indirect 90 | go.yaml.in/yaml/v2 v2.4.2 // indirect 91 | golang.org/x/arch v0.18.0 // indirect 92 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 93 | golang.org/x/net v0.41.0 // indirect 94 | golang.org/x/sys v0.33.0 // indirect 95 | golang.org/x/text v0.26.0 // indirect 96 | golang.org/x/time v0.3.0 // indirect 97 | golang.org/x/tools v0.34.0 // indirect 98 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect 99 | gopkg.in/ini.v1 v1.67.0 // indirect 100 | gopkg.in/yaml.v2 v2.4.0 // indirect 101 | gopkg.in/yaml.v3 v3.0.1 // indirect 102 | sigs.k8s.io/yaml v1.5.0 // indirect 103 | ) 104 | -------------------------------------------------------------------------------- /internal/application/user/handler/user.grpc.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "vibe-ddd-golang/api/proto/user" 7 | "vibe-ddd-golang/internal/application/user/dto" 8 | "vibe-ddd-golang/internal/application/user/service" 9 | 10 | "go.uber.org/zap" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | "google.golang.org/protobuf/types/known/timestamppb" 14 | ) 15 | 16 | type UserGrpcHandler struct { 17 | user.UnimplementedUserServiceServer 18 | userService service.UserService 19 | logger *zap.Logger 20 | } 21 | 22 | func NewUserGrpcHandler(userService service.UserService, logger *zap.Logger) *UserGrpcHandler { 23 | return &UserGrpcHandler{ 24 | userService: userService, 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (h *UserGrpcHandler) CreateUser( 30 | ctx context.Context, 31 | req *user.CreateUserRequest, 32 | ) (*user.CreateUserResponse, error) { 33 | createReq := &dto.CreateUserRequest{ 34 | Name: req.Name, 35 | Email: req.Email, 36 | Password: req.Password, 37 | } 38 | 39 | userResponse, err := h.userService.CreateUser(createReq) 40 | if err != nil { 41 | h.logger.Error("Failed to create user via gRPC", zap.Error(err)) 42 | return nil, status.Errorf(codes.Internal, "failed to create user: %v", err) 43 | } 44 | 45 | return &user.CreateUserResponse{ 46 | User: h.toProtoUser(userResponse), 47 | }, nil 48 | } 49 | 50 | func (h *UserGrpcHandler) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.GetUserResponse, error) { 51 | userResponse, err := h.userService.GetUserByID(uint(req.Id)) 52 | if err != nil { 53 | h.logger.Error("Failed to get user via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 54 | return nil, status.Errorf(codes.NotFound, "user not found: %v", err) 55 | } 56 | 57 | return &user.GetUserResponse{ 58 | User: h.toProtoUser(userResponse), 59 | }, nil 60 | } 61 | 62 | func (h *UserGrpcHandler) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) { 63 | page := int(req.Page) 64 | pageSize := int(req.PageSize) 65 | 66 | if page <= 0 { 67 | page = 1 68 | } 69 | if pageSize <= 0 { 70 | pageSize = 10 71 | } 72 | 73 | filter := &dto.UserFilter{ 74 | Page: page, 75 | PageSize: pageSize, 76 | } 77 | 78 | listResponse, err := h.userService.GetUsers(filter) 79 | if err != nil { 80 | h.logger.Error("Failed to list users via gRPC", zap.Error(err)) 81 | return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) 82 | } 83 | 84 | protoUsers := make([]*user.User, len(listResponse.Data)) 85 | for i, u := range listResponse.Data { 86 | protoUsers[i] = h.toProtoUser(&u) 87 | } 88 | 89 | return &user.ListUsersResponse{ 90 | Users: protoUsers, 91 | Total: listResponse.TotalCount, 92 | Page: int32(listResponse.Page), 93 | PageSize: int32(listResponse.PageSize), 94 | }, nil 95 | } 96 | 97 | func (h *UserGrpcHandler) UpdateUser( 98 | ctx context.Context, 99 | req *user.UpdateUserRequest, 100 | ) (*user.UpdateUserResponse, error) { 101 | updateReq := &dto.UpdateUserRequest{ 102 | Name: req.Name, 103 | Email: req.Email, 104 | } 105 | 106 | userResponse, err := h.userService.UpdateUser(uint(req.Id), updateReq) 107 | if err != nil { 108 | h.logger.Error("Failed to update user via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 109 | return nil, status.Errorf(codes.Internal, "failed to update user: %v", err) 110 | } 111 | 112 | return &user.UpdateUserResponse{ 113 | User: h.toProtoUser(userResponse), 114 | }, nil 115 | } 116 | 117 | func (h *UserGrpcHandler) DeleteUser( 118 | ctx context.Context, 119 | req *user.DeleteUserRequest, 120 | ) (*user.DeleteUserResponse, error) { 121 | err := h.userService.DeleteUser(uint(req.Id)) 122 | if err != nil { 123 | h.logger.Error("Failed to delete user via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 124 | return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err) 125 | } 126 | 127 | return &user.DeleteUserResponse{ 128 | Success: true, 129 | }, nil 130 | } 131 | 132 | func (h *UserGrpcHandler) UpdateUserPassword( 133 | ctx context.Context, 134 | req *user.UpdateUserPasswordRequest, 135 | ) (*user.UpdateUserPasswordResponse, error) { 136 | updateReq := &dto.UpdateUserPasswordRequest{ 137 | CurrentPassword: req.OldPassword, 138 | NewPassword: req.NewPassword, 139 | } 140 | 141 | err := h.userService.UpdateUserPassword(uint(req.Id), updateReq) 142 | if err != nil { 143 | h.logger.Error("Failed to update user password via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 144 | return nil, status.Errorf(codes.Internal, "failed to update password: %v", err) 145 | } 146 | 147 | return &user.UpdateUserPasswordResponse{ 148 | Success: true, 149 | }, nil 150 | } 151 | 152 | func (h *UserGrpcHandler) toProtoUser(u *dto.UserResponse) *user.User { 153 | return &user.User{ 154 | Id: uint32(u.ID), 155 | Name: u.Name, 156 | Email: u.Email, 157 | CreatedAt: timestamppb.New(u.CreatedAt), 158 | UpdatedAt: timestamppb.New(u.UpdatedAt), 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /internal/application/payment/service/payment.service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "vibe-ddd-golang/internal/application/payment/dto" 8 | "vibe-ddd-golang/internal/application/payment/entity" 9 | "vibe-ddd-golang/internal/application/payment/repository" 10 | "vibe-ddd-golang/internal/application/user/service" 11 | 12 | "go.uber.org/zap" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type PaymentService interface { 17 | CreatePayment(req *dto.CreatePaymentRequest) (*dto.PaymentResponse, error) 18 | GetPaymentByID(id uint) (*dto.PaymentResponse, error) 19 | GetPayments(filter *dto.PaymentFilter) (*dto.PaymentListResponse, error) 20 | UpdatePayment(id uint, req *dto.UpdatePaymentRequest) (*dto.PaymentResponse, error) 21 | DeletePayment(id uint) error 22 | GetPaymentsByUser(userID uint) ([]dto.PaymentResponse, error) 23 | } 24 | 25 | type paymentService struct { 26 | repo repository.PaymentRepository 27 | userService service.UserService 28 | logger *zap.Logger 29 | } 30 | 31 | func NewPaymentService( 32 | repo repository.PaymentRepository, 33 | userService service.UserService, 34 | logger *zap.Logger, 35 | ) PaymentService { 36 | return &paymentService{ 37 | repo: repo, 38 | userService: userService, 39 | logger: logger, 40 | } 41 | } 42 | 43 | func (s *paymentService) CreatePayment(req *dto.CreatePaymentRequest) (*dto.PaymentResponse, error) { 44 | // Validate that user exists before creating payment 45 | _, err := s.userService.GetUserByID(req.UserID) 46 | if err != nil { 47 | s.logger.Error("User not found for payment creation", zap.Uint("user_id", req.UserID), zap.Error(err)) 48 | return nil, errors.New("user not found") 49 | } 50 | 51 | payment := &entity.Payment{ 52 | Amount: req.Amount, 53 | Currency: req.Currency, 54 | Status: entity.PaymentStatusPending, 55 | Description: req.Description, 56 | UserID: req.UserID, 57 | CreatedAt: time.Now(), 58 | UpdatedAt: time.Now(), 59 | } 60 | 61 | err = s.repo.Create(payment) 62 | if err != nil { 63 | s.logger.Error("Failed to create payment", zap.Error(err)) 64 | return nil, err 65 | } 66 | 67 | return s.entityToResponse(payment), nil 68 | } 69 | 70 | func (s *paymentService) GetPaymentByID(id uint) (*dto.PaymentResponse, error) { 71 | payment, err := s.repo.GetByID(id) 72 | if err != nil { 73 | if errors.Is(err, gorm.ErrRecordNotFound) { 74 | return nil, errors.New("payment not found") 75 | } 76 | return nil, err 77 | } 78 | 79 | return s.entityToResponse(payment), nil 80 | } 81 | 82 | func (s *paymentService) GetPayments(filter *dto.PaymentFilter) (*dto.PaymentListResponse, error) { 83 | if filter.Page <= 0 { 84 | filter.Page = 1 85 | } 86 | if filter.PageSize <= 0 { 87 | filter.PageSize = 10 88 | } 89 | 90 | payments, totalCount, err := s.repo.GetAll(filter) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | responses := make([]dto.PaymentResponse, 0, len(payments)) 96 | for _, payment := range payments { 97 | responses = append(responses, *s.entityToResponse(&payment)) 98 | } 99 | 100 | return &dto.PaymentListResponse{ 101 | Data: responses, 102 | TotalCount: totalCount, 103 | Page: filter.Page, 104 | PageSize: filter.PageSize, 105 | }, nil 106 | } 107 | 108 | func (s *paymentService) UpdatePayment(id uint, req *dto.UpdatePaymentRequest) (*dto.PaymentResponse, error) { 109 | payment, err := s.repo.GetByID(id) 110 | if err != nil { 111 | if errors.Is(err, gorm.ErrRecordNotFound) { 112 | return nil, errors.New("payment not found") 113 | } 114 | return nil, err 115 | } 116 | 117 | status := entity.PaymentStatus(req.Status) 118 | if !status.IsValid() { 119 | return nil, errors.New("invalid payment status") 120 | } 121 | 122 | payment.Status = status 123 | if req.Description != "" { 124 | payment.Description = req.Description 125 | } 126 | payment.UpdatedAt = time.Now() 127 | 128 | err = s.repo.Update(payment) 129 | if err != nil { 130 | s.logger.Error("Failed to update payment", zap.Error(err)) 131 | return nil, err 132 | } 133 | 134 | return s.entityToResponse(payment), nil 135 | } 136 | 137 | func (s *paymentService) DeletePayment(id uint) error { 138 | _, err := s.repo.GetByID(id) 139 | if err != nil { 140 | if errors.Is(err, gorm.ErrRecordNotFound) { 141 | return errors.New("payment not found") 142 | } 143 | return err 144 | } 145 | 146 | return s.repo.Delete(id) 147 | } 148 | 149 | func (s *paymentService) GetPaymentsByUser(userID uint) ([]dto.PaymentResponse, error) { 150 | payments, err := s.repo.GetByUserID(userID) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | responses := make([]dto.PaymentResponse, 0, len(payments)) 156 | for _, payment := range payments { 157 | responses = append(responses, *s.entityToResponse(&payment)) 158 | } 159 | 160 | return responses, nil 161 | } 162 | 163 | func (s *paymentService) entityToResponse(payment *entity.Payment) *dto.PaymentResponse { 164 | return &dto.PaymentResponse{ 165 | ID: payment.ID, 166 | Amount: payment.Amount, 167 | Currency: payment.Currency, 168 | Status: payment.Status.String(), 169 | Description: payment.Description, 170 | UserID: payment.UserID, 171 | CreatedAt: payment.CreatedAt, 172 | UpdatedAt: payment.UpdatedAt, 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /internal/pkg/testutil/mocks.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "vibe-ddd-golang/internal/application/payment/dto" 5 | "vibe-ddd-golang/internal/application/payment/entity" 6 | userDto "vibe-ddd-golang/internal/application/user/dto" 7 | userEntity "vibe-ddd-golang/internal/application/user/entity" 8 | 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // MockUserRepository is a mock implementation of UserRepository 13 | type MockUserRepository struct { 14 | mock.Mock 15 | } 16 | 17 | func (m *MockUserRepository) Create(user *userEntity.User) error { 18 | args := m.Called(user) 19 | return args.Error(0) 20 | } 21 | 22 | func (m *MockUserRepository) GetByID(id uint) (*userEntity.User, error) { 23 | args := m.Called(id) 24 | if args.Get(0) == nil { 25 | return nil, args.Error(1) 26 | } 27 | return args.Get(0).(*userEntity.User), args.Error(1) 28 | } 29 | 30 | func (m *MockUserRepository) GetByEmail(email string) (*userEntity.User, error) { 31 | args := m.Called(email) 32 | if args.Get(0) == nil { 33 | return nil, args.Error(1) 34 | } 35 | return args.Get(0).(*userEntity.User), args.Error(1) 36 | } 37 | 38 | func (m *MockUserRepository) GetAll(filter *userDto.UserFilter) ([]userEntity.User, int64, error) { 39 | args := m.Called(filter) 40 | 41 | // Handle nil case safely 42 | var users []userEntity.User 43 | if args.Get(0) != nil { 44 | users = args.Get(0).([]userEntity.User) 45 | } 46 | 47 | var count int64 48 | if args.Get(1) != nil { 49 | count = args.Get(1).(int64) 50 | } 51 | 52 | return users, count, args.Error(2) 53 | } 54 | 55 | func (m *MockUserRepository) Update(user *userEntity.User) error { 56 | args := m.Called(user) 57 | return args.Error(0) 58 | } 59 | 60 | func (m *MockUserRepository) Delete(id uint) error { 61 | args := m.Called(id) 62 | return args.Error(0) 63 | } 64 | 65 | func (m *MockUserRepository) EmailExists(email string) (bool, error) { 66 | args := m.Called(email) 67 | return args.Bool(0), args.Error(1) 68 | } 69 | 70 | // MockPaymentRepository is a mock implementation of PaymentRepository 71 | type MockPaymentRepository struct { 72 | mock.Mock 73 | } 74 | 75 | func (m *MockPaymentRepository) Create(payment *entity.Payment) error { 76 | args := m.Called(payment) 77 | return args.Error(0) 78 | } 79 | 80 | func (m *MockPaymentRepository) GetByID(id uint) (*entity.Payment, error) { 81 | args := m.Called(id) 82 | if args.Get(0) == nil { 83 | return nil, args.Error(1) 84 | } 85 | return args.Get(0).(*entity.Payment), args.Error(1) 86 | } 87 | 88 | func (m *MockPaymentRepository) GetAll(filter *dto.PaymentFilter) ([]entity.Payment, int64, error) { 89 | args := m.Called(filter) 90 | 91 | // Handle nil case safely 92 | var payments []entity.Payment 93 | if args.Get(0) != nil { 94 | payments = args.Get(0).([]entity.Payment) 95 | } 96 | 97 | var count int64 98 | if args.Get(1) != nil { 99 | count = args.Get(1).(int64) 100 | } 101 | 102 | return payments, count, args.Error(2) 103 | } 104 | 105 | func (m *MockPaymentRepository) Update(payment *entity.Payment) error { 106 | args := m.Called(payment) 107 | return args.Error(0) 108 | } 109 | 110 | func (m *MockPaymentRepository) Delete(id uint) error { 111 | args := m.Called(id) 112 | return args.Error(0) 113 | } 114 | 115 | func (m *MockPaymentRepository) GetByUserID(userID uint) ([]entity.Payment, error) { 116 | args := m.Called(userID) 117 | 118 | // Handle nil case safely 119 | var payments []entity.Payment 120 | if args.Get(0) != nil { 121 | payments = args.Get(0).([]entity.Payment) 122 | } 123 | 124 | return payments, args.Error(1) 125 | } 126 | 127 | // MockUserService is a mock implementation of UserService 128 | type MockUserService struct { 129 | mock.Mock 130 | } 131 | 132 | func (m *MockUserService) CreateUser(req *userDto.CreateUserRequest) (*userDto.UserResponse, error) { 133 | args := m.Called(req) 134 | if args.Get(0) == nil { 135 | return nil, args.Error(1) 136 | } 137 | return args.Get(0).(*userDto.UserResponse), args.Error(1) 138 | } 139 | 140 | func (m *MockUserService) GetUserByID(id uint) (*userDto.UserResponse, error) { 141 | args := m.Called(id) 142 | if args.Get(0) == nil { 143 | return nil, args.Error(1) 144 | } 145 | return args.Get(0).(*userDto.UserResponse), args.Error(1) 146 | } 147 | 148 | func (m *MockUserService) GetUserByEmail(email string) (*userDto.UserResponse, error) { 149 | args := m.Called(email) 150 | if args.Get(0) == nil { 151 | return nil, args.Error(1) 152 | } 153 | return args.Get(0).(*userDto.UserResponse), args.Error(1) 154 | } 155 | 156 | func (m *MockUserService) GetUsers(filter *userDto.UserFilter) (*userDto.UserListResponse, error) { 157 | args := m.Called(filter) 158 | if args.Get(0) == nil { 159 | return nil, args.Error(1) 160 | } 161 | return args.Get(0).(*userDto.UserListResponse), args.Error(1) 162 | } 163 | 164 | func (m *MockUserService) UpdateUser(id uint, req *userDto.UpdateUserRequest) (*userDto.UserResponse, error) { 165 | args := m.Called(id, req) 166 | if args.Get(0) == nil { 167 | return nil, args.Error(1) 168 | } 169 | return args.Get(0).(*userDto.UserResponse), args.Error(1) 170 | } 171 | 172 | func (m *MockUserService) UpdateUserPassword(id uint, req *userDto.UpdateUserPasswordRequest) error { 173 | args := m.Called(id, req) 174 | return args.Error(0) 175 | } 176 | 177 | func (m *MockUserService) DeleteUser(id uint) error { 178 | args := m.Called(id) 179 | return args.Error(0) 180 | } 181 | -------------------------------------------------------------------------------- /internal/application/user/service/user.service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "vibe-ddd-golang/internal/application/user/dto" 8 | "vibe-ddd-golang/internal/application/user/entity" 9 | "vibe-ddd-golang/internal/application/user/repository" 10 | 11 | "go.uber.org/zap" 12 | "golang.org/x/crypto/bcrypt" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type UserService interface { 17 | CreateUser(req *dto.CreateUserRequest) (*dto.UserResponse, error) 18 | GetUserByID(id uint) (*dto.UserResponse, error) 19 | GetUserByEmail(email string) (*dto.UserResponse, error) 20 | GetUsers(filter *dto.UserFilter) (*dto.UserListResponse, error) 21 | UpdateUser(id uint, req *dto.UpdateUserRequest) (*dto.UserResponse, error) 22 | UpdateUserPassword(id uint, req *dto.UpdateUserPasswordRequest) error 23 | DeleteUser(id uint) error 24 | } 25 | 26 | type userService struct { 27 | repo repository.UserRepository 28 | logger *zap.Logger 29 | } 30 | 31 | func NewUserService(repo repository.UserRepository, logger *zap.Logger) UserService { 32 | return &userService{ 33 | repo: repo, 34 | logger: logger, 35 | } 36 | } 37 | 38 | func (s *userService) CreateUser(req *dto.CreateUserRequest) (*dto.UserResponse, error) { 39 | exists, err := s.repo.EmailExists(req.Email) 40 | if err != nil { 41 | s.logger.Error("Failed to check email existence", zap.Error(err)) 42 | return nil, err 43 | } 44 | if exists { 45 | return nil, errors.New("email already exists") 46 | } 47 | 48 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) 49 | if err != nil { 50 | s.logger.Error("Failed to hash password", zap.Error(err)) 51 | return nil, err 52 | } 53 | 54 | user := &entity.User{ 55 | Name: req.Name, 56 | Email: req.Email, 57 | Password: string(hashedPassword), 58 | CreatedAt: time.Now(), 59 | UpdatedAt: time.Now(), 60 | } 61 | 62 | err = s.repo.Create(user) 63 | if err != nil { 64 | s.logger.Error("Failed to create user", zap.Error(err)) 65 | return nil, err 66 | } 67 | 68 | return s.entityToResponse(user), nil 69 | } 70 | 71 | func (s *userService) GetUserByID(id uint) (*dto.UserResponse, error) { 72 | user, err := s.repo.GetByID(id) 73 | if err != nil { 74 | if errors.Is(err, gorm.ErrRecordNotFound) { 75 | return nil, errors.New("user not found") 76 | } 77 | return nil, err 78 | } 79 | 80 | return s.entityToResponse(user), nil 81 | } 82 | 83 | func (s *userService) GetUserByEmail(email string) (*dto.UserResponse, error) { 84 | user, err := s.repo.GetByEmail(email) 85 | if err != nil { 86 | if errors.Is(err, gorm.ErrRecordNotFound) { 87 | return nil, errors.New("user not found") 88 | } 89 | return nil, err 90 | } 91 | 92 | return s.entityToResponse(user), nil 93 | } 94 | 95 | func (s *userService) GetUsers(filter *dto.UserFilter) (*dto.UserListResponse, error) { 96 | if filter.Page <= 0 { 97 | filter.Page = 1 98 | } 99 | if filter.PageSize <= 0 { 100 | filter.PageSize = 10 101 | } 102 | 103 | users, totalCount, err := s.repo.GetAll(filter) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | responses := make([]dto.UserResponse, 0, len(users)) 109 | for _, user := range users { 110 | responses = append(responses, *s.entityToResponse(&user)) 111 | } 112 | 113 | return &dto.UserListResponse{ 114 | Data: responses, 115 | TotalCount: totalCount, 116 | Page: filter.Page, 117 | PageSize: filter.PageSize, 118 | }, nil 119 | } 120 | 121 | func (s *userService) UpdateUser(id uint, req *dto.UpdateUserRequest) (*dto.UserResponse, error) { 122 | user, err := s.repo.GetByID(id) 123 | if err != nil { 124 | if errors.Is(err, gorm.ErrRecordNotFound) { 125 | return nil, errors.New("user not found") 126 | } 127 | return nil, err 128 | } 129 | 130 | if req.Email != user.Email { 131 | exists, err := s.repo.EmailExists(req.Email) 132 | if err != nil { 133 | s.logger.Error("Failed to check email existence", zap.Error(err)) 134 | return nil, err 135 | } 136 | if exists { 137 | return nil, errors.New("email already exists") 138 | } 139 | } 140 | 141 | user.Name = req.Name 142 | user.Email = req.Email 143 | user.UpdatedAt = time.Now() 144 | 145 | err = s.repo.Update(user) 146 | if err != nil { 147 | s.logger.Error("Failed to update user", zap.Error(err)) 148 | return nil, err 149 | } 150 | 151 | return s.entityToResponse(user), nil 152 | } 153 | 154 | func (s *userService) UpdateUserPassword(id uint, req *dto.UpdateUserPasswordRequest) error { 155 | user, err := s.repo.GetByID(id) 156 | if err != nil { 157 | if errors.Is(err, gorm.ErrRecordNotFound) { 158 | return errors.New("user not found") 159 | } 160 | return err 161 | } 162 | 163 | err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.CurrentPassword)) 164 | if err != nil { 165 | return errors.New("current password is incorrect") 166 | } 167 | 168 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) 169 | if err != nil { 170 | s.logger.Error("Failed to hash new password", zap.Error(err)) 171 | return err 172 | } 173 | 174 | user.Password = string(hashedPassword) 175 | user.UpdatedAt = time.Now() 176 | 177 | return s.repo.Update(user) 178 | } 179 | 180 | func (s *userService) DeleteUser(id uint) error { 181 | _, err := s.repo.GetByID(id) 182 | if err != nil { 183 | if errors.Is(err, gorm.ErrRecordNotFound) { 184 | return errors.New("user not found") 185 | } 186 | return err 187 | } 188 | 189 | return s.repo.Delete(id) 190 | } 191 | 192 | func (s *userService) entityToResponse(user *entity.User) *dto.UserResponse { 193 | return &dto.UserResponse{ 194 | ID: user.ID, 195 | Name: user.Name, 196 | Email: user.Email, 197 | CreatedAt: user.CreatedAt, 198 | UpdatedAt: user.UpdatedAt, 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /internal/application/payment/handler/payment.grpc.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "vibe-ddd-golang/api/proto/payment" 7 | "vibe-ddd-golang/internal/application/payment/dto" 8 | "vibe-ddd-golang/internal/application/payment/entity" 9 | "vibe-ddd-golang/internal/application/payment/service" 10 | 11 | "go.uber.org/zap" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | "google.golang.org/protobuf/types/known/timestamppb" 15 | ) 16 | 17 | type PaymentGrpcHandler struct { 18 | payment.UnimplementedPaymentServiceServer 19 | paymentService service.PaymentService 20 | logger *zap.Logger 21 | } 22 | 23 | func NewPaymentGrpcHandler(paymentService service.PaymentService, logger *zap.Logger) *PaymentGrpcHandler { 24 | return &PaymentGrpcHandler{ 25 | paymentService: paymentService, 26 | logger: logger, 27 | } 28 | } 29 | 30 | func (h *PaymentGrpcHandler) CreatePayment( 31 | ctx context.Context, 32 | req *payment.CreatePaymentRequest, 33 | ) (*payment.CreatePaymentResponse, error) { 34 | createReq := &dto.CreatePaymentRequest{ 35 | Amount: req.Amount, 36 | Currency: req.Currency, 37 | Description: req.Description, 38 | UserID: uint(req.UserId), 39 | } 40 | 41 | paymentResponse, err := h.paymentService.CreatePayment(createReq) 42 | if err != nil { 43 | h.logger.Error("Failed to create payment via gRPC", zap.Error(err)) 44 | return nil, status.Errorf(codes.Internal, "failed to create payment: %v", err) 45 | } 46 | 47 | return &payment.CreatePaymentResponse{ 48 | Payment: h.toProtoPayment(paymentResponse), 49 | }, nil 50 | } 51 | 52 | func (h *PaymentGrpcHandler) GetPayment( 53 | ctx context.Context, 54 | req *payment.GetPaymentRequest, 55 | ) (*payment.GetPaymentResponse, error) { 56 | paymentResponse, err := h.paymentService.GetPaymentByID(uint(req.Id)) 57 | if err != nil { 58 | h.logger.Error("Failed to get payment via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 59 | return nil, status.Errorf(codes.NotFound, "payment not found: %v", err) 60 | } 61 | 62 | return &payment.GetPaymentResponse{ 63 | Payment: h.toProtoPayment(paymentResponse), 64 | }, nil 65 | } 66 | 67 | func (h *PaymentGrpcHandler) ListPayments( 68 | ctx context.Context, 69 | req *payment.ListPaymentsRequest, 70 | ) (*payment.ListPaymentsResponse, error) { 71 | page := int(req.Page) 72 | pageSize := int(req.PageSize) 73 | 74 | if page <= 0 { 75 | page = 1 76 | } 77 | if pageSize <= 0 { 78 | pageSize = 10 79 | } 80 | 81 | filter := &dto.PaymentFilter{ 82 | Page: page, 83 | PageSize: pageSize, 84 | } 85 | 86 | // Add status filter if provided 87 | if req.Status != payment.PaymentStatus_PAYMENT_STATUS_UNSPECIFIED { 88 | filter.Status = h.protoStatusToString(req.Status) 89 | } 90 | 91 | // Add user filter if provided 92 | if req.UserId > 0 { 93 | filter.UserID = uint(req.UserId) 94 | } 95 | 96 | listResponse, err := h.paymentService.GetPayments(filter) 97 | if err != nil { 98 | h.logger.Error("Failed to list payments via gRPC", zap.Error(err)) 99 | return nil, status.Errorf(codes.Internal, "failed to list payments: %v", err) 100 | } 101 | 102 | protoPayments := make([]*payment.Payment, len(listResponse.Data)) 103 | for i, p := range listResponse.Data { 104 | protoPayments[i] = h.toProtoPayment(&p) 105 | } 106 | 107 | return &payment.ListPaymentsResponse{ 108 | Payments: protoPayments, 109 | Total: listResponse.TotalCount, 110 | Page: int32(listResponse.Page), 111 | PageSize: int32(listResponse.PageSize), 112 | }, nil 113 | } 114 | 115 | func (h *PaymentGrpcHandler) UpdatePayment( 116 | ctx context.Context, 117 | req *payment.UpdatePaymentRequest, 118 | ) (*payment.UpdatePaymentResponse, error) { 119 | updateReq := &dto.UpdatePaymentRequest{ 120 | Description: req.Description, 121 | } 122 | 123 | // Add status if provided 124 | if req.Status != payment.PaymentStatus_PAYMENT_STATUS_UNSPECIFIED { 125 | updateReq.Status = h.protoStatusToString(req.Status) 126 | } 127 | 128 | paymentResponse, err := h.paymentService.UpdatePayment(uint(req.Id), updateReq) 129 | if err != nil { 130 | h.logger.Error("Failed to update payment via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 131 | return nil, status.Errorf(codes.Internal, "failed to update payment: %v", err) 132 | } 133 | 134 | return &payment.UpdatePaymentResponse{ 135 | Payment: h.toProtoPayment(paymentResponse), 136 | }, nil 137 | } 138 | 139 | func (h *PaymentGrpcHandler) DeletePayment( 140 | ctx context.Context, 141 | req *payment.DeletePaymentRequest, 142 | ) (*payment.DeletePaymentResponse, error) { 143 | err := h.paymentService.DeletePayment(uint(req.Id)) 144 | if err != nil { 145 | h.logger.Error("Failed to delete payment via gRPC", zap.Uint32("id", req.Id), zap.Error(err)) 146 | return nil, status.Errorf(codes.Internal, "failed to delete payment: %v", err) 147 | } 148 | 149 | return &payment.DeletePaymentResponse{ 150 | Success: true, 151 | }, nil 152 | } 153 | 154 | func (h *PaymentGrpcHandler) GetUserPayments( 155 | ctx context.Context, 156 | req *payment.GetUserPaymentsRequest, 157 | ) (*payment.GetUserPaymentsResponse, error) { 158 | page := int(req.Page) 159 | pageSize := int(req.PageSize) 160 | 161 | if page <= 0 { 162 | page = 1 163 | } 164 | if pageSize <= 0 { 165 | pageSize = 10 166 | } 167 | 168 | filter := &dto.PaymentFilter{ 169 | Page: page, 170 | PageSize: pageSize, 171 | UserID: uint(req.UserId), 172 | } 173 | 174 | listResponse, err := h.paymentService.GetPayments(filter) 175 | if err != nil { 176 | h.logger.Error("Failed to get user payments via gRPC", zap.Uint32("user_id", req.UserId), zap.Error(err)) 177 | return nil, status.Errorf(codes.Internal, "failed to get user payments: %v", err) 178 | } 179 | 180 | protoPayments := make([]*payment.Payment, len(listResponse.Data)) 181 | for i, p := range listResponse.Data { 182 | protoPayments[i] = h.toProtoPayment(&p) 183 | } 184 | 185 | return &payment.GetUserPaymentsResponse{ 186 | Payments: protoPayments, 187 | Total: listResponse.TotalCount, 188 | Page: int32(listResponse.Page), 189 | PageSize: int32(listResponse.PageSize), 190 | }, nil 191 | } 192 | 193 | func (h *PaymentGrpcHandler) toProtoPayment(p *dto.PaymentResponse) *payment.Payment { 194 | return &payment.Payment{ 195 | Id: uint32(p.ID), 196 | Amount: p.Amount, 197 | Currency: p.Currency, 198 | Description: p.Description, 199 | Status: h.stringStatusToProto(p.Status), 200 | UserId: uint32(p.UserID), 201 | CreatedAt: timestamppb.New(p.CreatedAt), 202 | UpdatedAt: timestamppb.New(p.UpdatedAt), 203 | } 204 | } 205 | 206 | func (h *PaymentGrpcHandler) stringStatusToProto(status string) payment.PaymentStatus { 207 | switch status { 208 | case entity.PaymentStatusPending.String(): 209 | return payment.PaymentStatus_PAYMENT_STATUS_PENDING 210 | case entity.PaymentStatusCompleted.String(): 211 | return payment.PaymentStatus_PAYMENT_STATUS_COMPLETED 212 | case entity.PaymentStatusFailed.String(): 213 | return payment.PaymentStatus_PAYMENT_STATUS_FAILED 214 | case entity.PaymentStatusCanceled.String(): 215 | return payment.PaymentStatus_PAYMENT_STATUS_CANCELED 216 | default: 217 | return payment.PaymentStatus_PAYMENT_STATUS_UNSPECIFIED 218 | } 219 | } 220 | 221 | func (h *PaymentGrpcHandler) protoStatusToString(status payment.PaymentStatus) string { 222 | switch status { 223 | case payment.PaymentStatus_PAYMENT_STATUS_PENDING: 224 | return entity.PaymentStatusPending.String() 225 | case payment.PaymentStatus_PAYMENT_STATUS_COMPLETED: 226 | return entity.PaymentStatusCompleted.String() 227 | case payment.PaymentStatus_PAYMENT_STATUS_FAILED: 228 | return entity.PaymentStatusFailed.String() 229 | case payment.PaymentStatus_PAYMENT_STATUS_CANCELED: 230 | return entity.PaymentStatusCanceled.String() 231 | default: 232 | return entity.PaymentStatusPending.String() 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /internal/application/payment/handler/payment.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "vibe-ddd-golang/internal/application/payment/dto" 8 | "vibe-ddd-golang/internal/application/payment/service" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type PaymentHandler struct { 15 | service service.PaymentService 16 | logger *zap.Logger 17 | } 18 | 19 | func NewPaymentHandler(service service.PaymentService, logger *zap.Logger) *PaymentHandler { 20 | return &PaymentHandler{ 21 | service: service, 22 | logger: logger, 23 | } 24 | } 25 | 26 | // CreatePayment godoc 27 | // @Summary Create a new payment 28 | // @Description Create a new payment with the provided information 29 | // @Tags payments 30 | // @Accept json 31 | // @Produce json 32 | // @Param payment body dto.CreatePaymentRequest true "Payment creation request" 33 | // @Success 201 {object} map[string]interface{} "Created payment" 34 | // @Failure 400 {object} map[string]interface{} "Invalid request body" 35 | // @Failure 500 {object} map[string]interface{} "Internal server error" 36 | // @Router /payments [post] 37 | func (h *PaymentHandler) CreatePayment(ctx *gin.Context) { 38 | var req dto.CreatePaymentRequest 39 | if err := ctx.ShouldBindJSON(&req); err != nil { 40 | h.logger.Error("Invalid request body", zap.Error(err)) 41 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 42 | return 43 | } 44 | 45 | payment, err := h.service.CreatePayment(&req) 46 | if err != nil { 47 | h.logger.Error("Failed to create payment", zap.Error(err)) 48 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create payment"}) 49 | return 50 | } 51 | 52 | ctx.JSON(http.StatusCreated, gin.H{"data": payment}) 53 | } 54 | 55 | // GetPayment godoc 56 | // @Summary Get a payment by ID 57 | // @Description Get a single payment by its ID 58 | // @Tags payments 59 | // @Accept json 60 | // @Produce json 61 | // @Param id path int true "Payment ID" 62 | // @Success 200 {object} map[string]interface{} "Payment details" 63 | // @Failure 400 {object} map[string]interface{} "Invalid payment ID" 64 | // @Failure 404 {object} map[string]interface{} "Payment not found" 65 | // @Router /payments/{id} [get] 66 | func (h *PaymentHandler) GetPayment(ctx *gin.Context) { 67 | idStr := ctx.Param("id") 68 | id, err := strconv.ParseUint(idStr, 10, 32) 69 | if err != nil { 70 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payment ID"}) 71 | return 72 | } 73 | 74 | payment, err := h.service.GetPaymentByID(uint(id)) 75 | if err != nil { 76 | h.logger.Error("Failed to get payment", zap.Error(err)) 77 | ctx.JSON(http.StatusNotFound, gin.H{"error": "Payment not found"}) 78 | return 79 | } 80 | 81 | ctx.JSON(http.StatusOK, gin.H{"data": payment}) 82 | } 83 | 84 | // GetPayments godoc 85 | // @Summary Get all payments 86 | // @Description Get a list of payments with optional filtering and pagination 87 | // @Tags payments 88 | // @Accept json 89 | // @Produce json 90 | // @Param status query string false "Filter by status" Enums(pending, completed, failed, canceled) 91 | // @Param currency query string false "Filter by currency (3-letter code)" 92 | // @Param user_id query int false "Filter by user ID" 93 | // @Param page query int false "Page number" default(1) 94 | // @Param page_size query int false "Number of items per page" default(10) 95 | // @Success 200 {object} dto.PaymentListResponse "List of payments" 96 | // @Failure 400 {object} map[string]interface{} "Invalid query parameters" 97 | // @Failure 500 {object} map[string]interface{} "Internal server error" 98 | // @Router /payments [get] 99 | func (h *PaymentHandler) GetPayments(ctx *gin.Context) { 100 | var filter dto.PaymentFilter 101 | if err := ctx.ShouldBindQuery(&filter); err != nil { 102 | h.logger.Error("Invalid query parameters", zap.Error(err)) 103 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | payments, err := h.service.GetPayments(&filter) 108 | if err != nil { 109 | h.logger.Error("Failed to get payments", zap.Error(err)) 110 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get payments"}) 111 | return 112 | } 113 | 114 | ctx.JSON(http.StatusOK, payments) 115 | } 116 | 117 | // UpdatePayment godoc 118 | // @Summary Update a payment 119 | // @Description Update a payment's information by ID 120 | // @Tags payments 121 | // @Accept json 122 | // @Produce json 123 | // @Param id path int true "Payment ID" 124 | // @Param payment body dto.UpdatePaymentRequest true "Payment update request" 125 | // @Success 200 {object} map[string]interface{} "Updated payment" 126 | // @Failure 400 {object} map[string]interface{} "Invalid request" 127 | // @Failure 500 {object} map[string]interface{} "Internal server error" 128 | // @Router /payments/{id} [put] 129 | func (h *PaymentHandler) UpdatePayment(ctx *gin.Context) { 130 | idStr := ctx.Param("id") 131 | id, err := strconv.ParseUint(idStr, 10, 32) 132 | if err != nil { 133 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payment ID"}) 134 | return 135 | } 136 | 137 | var req dto.UpdatePaymentRequest 138 | if err := ctx.ShouldBindJSON(&req); err != nil { 139 | h.logger.Error("Invalid request body", zap.Error(err)) 140 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 141 | return 142 | } 143 | 144 | payment, err := h.service.UpdatePayment(uint(id), &req) 145 | if err != nil { 146 | h.logger.Error("Failed to update payment", zap.Error(err)) 147 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update payment"}) 148 | return 149 | } 150 | 151 | ctx.JSON(http.StatusOK, gin.H{"data": payment}) 152 | } 153 | 154 | // DeletePayment godoc 155 | // @Summary Delete a payment 156 | // @Description Delete a payment by ID 157 | // @Tags payments 158 | // @Accept json 159 | // @Produce json 160 | // @Param id path int true "Payment ID" 161 | // @Success 200 {object} map[string]interface{} "Payment deleted successfully" 162 | // @Failure 400 {object} map[string]interface{} "Invalid payment ID" 163 | // @Failure 500 {object} map[string]interface{} "Internal server error" 164 | // @Router /payments/{id} [delete] 165 | func (h *PaymentHandler) DeletePayment(ctx *gin.Context) { 166 | idStr := ctx.Param("id") 167 | id, err := strconv.ParseUint(idStr, 10, 32) 168 | if err != nil { 169 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payment ID"}) 170 | return 171 | } 172 | 173 | err = h.service.DeletePayment(uint(id)) 174 | if err != nil { 175 | h.logger.Error("Failed to delete payment", zap.Error(err)) 176 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete payment"}) 177 | return 178 | } 179 | 180 | ctx.JSON(http.StatusOK, gin.H{"message": "Payment deleted successfully"}) 181 | } 182 | 183 | func (h *PaymentHandler) RegisterRoutes(api *gin.RouterGroup) { 184 | payments := api.Group("/payments") 185 | { 186 | payments.POST("", h.CreatePayment) 187 | payments.GET("", h.GetPayments) 188 | payments.GET("/:id", h.GetPayment) 189 | payments.PUT("/:id", h.UpdatePayment) 190 | payments.DELETE("/:id", h.DeletePayment) 191 | } 192 | 193 | users := api.Group("/users") 194 | { 195 | users.GET("/:id/payments", h.GetPaymentsByUser) 196 | } 197 | } 198 | 199 | // GetPaymentsByUser godoc 200 | // @Summary Get payments by user ID 201 | // @Description Get all payments for a specific user 202 | // @Tags payments 203 | // @Accept json 204 | // @Produce json 205 | // @Param id path int true "User ID" 206 | // @Success 200 {object} map[string]interface{} "List of payments for the user" 207 | // @Failure 400 {object} map[string]interface{} "Invalid user ID" 208 | // @Failure 500 {object} map[string]interface{} "Internal server error" 209 | // @Router /users/{id}/payments [get] 210 | func (h *PaymentHandler) GetPaymentsByUser(ctx *gin.Context) { 211 | userIDStr := ctx.Param("id") 212 | userID, err := strconv.ParseUint(userIDStr, 10, 32) 213 | if err != nil { 214 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) 215 | return 216 | } 217 | 218 | payments, err := h.service.GetPaymentsByUser(uint(userID)) 219 | if err != nil { 220 | h.logger.Error("Failed to get payments by user", zap.Error(err)) 221 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get payments"}) 222 | return 223 | } 224 | 225 | ctx.JSON(http.StatusOK, gin.H{"data": payments}) 226 | } 227 | -------------------------------------------------------------------------------- /internal/application/user/repository/user.repo_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "vibe-ddd-golang/internal/application/user/dto" 8 | "vibe-ddd-golang/internal/application/user/entity" 9 | "vibe-ddd-golang/internal/pkg/testutil" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func TestUserRepository_Create(t *testing.T) { 17 | // Setup 18 | db, err := testutil.SetupTestDB() 19 | require.NoError(t, err) 20 | logger := testutil.NewTestLogger(t) 21 | repo := NewUserRepository(db, logger) 22 | 23 | t.Run("should create user successfully", func(t *testing.T) { 24 | // Given 25 | user := testutil.CreateUserFixture() 26 | user.ID = 0 // Reset ID for creation 27 | 28 | // When 29 | err := repo.Create(user) 30 | 31 | // Then 32 | assert.NoError(t, err) 33 | assert.NotZero(t, user.ID) 34 | 35 | // Verify user was created in database 36 | var dbUser entity.User 37 | err = db.First(&dbUser, user.ID).Error 38 | assert.NoError(t, err) 39 | assert.Equal(t, user.Email, dbUser.Email) 40 | assert.Equal(t, user.Name, dbUser.Name) 41 | }) 42 | 43 | t.Run("should fail to create user with duplicate email", func(t *testing.T) { 44 | // Given 45 | user1 := testutil.CreateUserFixture() 46 | user1.ID = 0 47 | user1.Email = "duplicate@example.com" 48 | 49 | user2 := testutil.CreateUserFixture() 50 | user2.ID = 0 51 | user2.Email = "duplicate@example.com" 52 | 53 | // When 54 | err1 := repo.Create(user1) 55 | err2 := repo.Create(user2) 56 | 57 | // Then 58 | assert.NoError(t, err1) 59 | assert.Error(t, err2) // Should fail due to unique constraint 60 | }) 61 | 62 | // Cleanup 63 | testutil.CleanDB(db) 64 | } 65 | 66 | func TestUserRepository_GetByID(t *testing.T) { 67 | // Setup 68 | db, err := testutil.SetupTestDB() 69 | require.NoError(t, err) 70 | logger := testutil.NewTestLogger(t) 71 | repo := NewUserRepository(db, logger) 72 | 73 | t.Run("should get user by ID successfully", func(t *testing.T) { 74 | // Given 75 | user := testutil.CreateUserFixture() 76 | user.ID = 0 77 | err := repo.Create(user) 78 | require.NoError(t, err) 79 | 80 | // When 81 | foundUser, err := repo.GetByID(user.ID) 82 | 83 | // Then 84 | assert.NoError(t, err) 85 | assert.Equal(t, user.ID, foundUser.ID) 86 | assert.Equal(t, user.Email, foundUser.Email) 87 | assert.Equal(t, user.Name, foundUser.Name) 88 | }) 89 | 90 | t.Run("should return error when user not found", func(t *testing.T) { 91 | // When 92 | _, err := repo.GetByID(999) 93 | 94 | // Then 95 | assert.Error(t, err) 96 | assert.Equal(t, gorm.ErrRecordNotFound, err) 97 | }) 98 | 99 | // Cleanup 100 | testutil.CleanDB(db) 101 | } 102 | 103 | func TestUserRepository_GetByEmail(t *testing.T) { 104 | // Setup 105 | db, err := testutil.SetupTestDB() 106 | require.NoError(t, err) 107 | logger := testutil.NewTestLogger(t) 108 | repo := NewUserRepository(db, logger) 109 | 110 | t.Run("should get user by email successfully", func(t *testing.T) { 111 | // Given 112 | user := testutil.CreateUserFixture() 113 | user.ID = 0 114 | err := repo.Create(user) 115 | require.NoError(t, err) 116 | 117 | // When 118 | foundUser, err := repo.GetByEmail(user.Email) 119 | 120 | // Then 121 | assert.NoError(t, err) 122 | assert.Equal(t, user.ID, foundUser.ID) 123 | assert.Equal(t, user.Email, foundUser.Email) 124 | assert.Equal(t, user.Name, foundUser.Name) 125 | }) 126 | 127 | t.Run("should return error when user email not found", func(t *testing.T) { 128 | // When 129 | _, err := repo.GetByEmail("nonexistent@example.com") 130 | 131 | // Then 132 | assert.Error(t, err) 133 | assert.Equal(t, gorm.ErrRecordNotFound, err) 134 | }) 135 | 136 | // Cleanup 137 | testutil.CleanDB(db) 138 | } 139 | 140 | func TestUserRepository_GetAll(t *testing.T) { 141 | // Setup 142 | db, err := testutil.SetupTestDB() 143 | require.NoError(t, err) 144 | logger := testutil.NewTestLogger(t) 145 | repo := NewUserRepository(db, logger) 146 | 147 | t.Run("should get all users with pagination", func(t *testing.T) { 148 | // Given - Create multiple users 149 | for i := 0; i < 5; i++ { 150 | user := testutil.CreateUserFixture() 151 | user.ID = 0 152 | user.Email = fmt.Sprintf("user%d@example.com", i) 153 | user.Name = fmt.Sprintf("User %d", i) 154 | err := repo.Create(user) 155 | require.NoError(t, err) 156 | } 157 | 158 | filter := &dto.UserFilter{ 159 | Page: 1, 160 | PageSize: 3, 161 | } 162 | 163 | // When 164 | users, totalCount, err := repo.GetAll(filter) 165 | 166 | // Then 167 | assert.NoError(t, err) 168 | assert.Len(t, users, 3) // Should return 3 users due to page size 169 | assert.Equal(t, int64(5), totalCount) // Total count should be 5 170 | }) 171 | 172 | t.Run("should filter users by name", func(t *testing.T) { 173 | // Given 174 | user1 := testutil.CreateUserFixture() 175 | user1.ID = 0 176 | user1.Email = "alice@example.com" 177 | user1.Name = "Alice Smith" 178 | err := repo.Create(user1) 179 | require.NoError(t, err) 180 | 181 | user2 := testutil.CreateUserFixture() 182 | user2.ID = 0 183 | user2.Email = "bob@example.com" 184 | user2.Name = "Bob Johnson" 185 | err = repo.Create(user2) 186 | require.NoError(t, err) 187 | 188 | filter := &dto.UserFilter{ 189 | Name: "Alice", 190 | } 191 | 192 | // When 193 | users, totalCount, err := repo.GetAll(filter) 194 | 195 | // Then 196 | assert.NoError(t, err) 197 | assert.Len(t, users, 1) 198 | assert.Equal(t, int64(1), totalCount) 199 | assert.Equal(t, "Alice Smith", users[0].Name) 200 | }) 201 | 202 | // Cleanup 203 | testutil.CleanDB(db) 204 | } 205 | 206 | func TestUserRepository_Update(t *testing.T) { 207 | // Setup 208 | db, err := testutil.SetupTestDB() 209 | require.NoError(t, err) 210 | logger := testutil.NewTestLogger(t) 211 | repo := NewUserRepository(db, logger) 212 | 213 | t.Run("should update user successfully", func(t *testing.T) { 214 | // Given 215 | user := testutil.CreateUserFixture() 216 | user.ID = 0 217 | err := repo.Create(user) 218 | require.NoError(t, err) 219 | 220 | // When 221 | user.Name = "Updated Name" 222 | user.Email = "updated@example.com" 223 | err = repo.Update(user) 224 | 225 | // Then 226 | assert.NoError(t, err) 227 | 228 | // Verify update in database 229 | var dbUser entity.User 230 | err = db.First(&dbUser, user.ID).Error 231 | assert.NoError(t, err) 232 | assert.Equal(t, "Updated Name", dbUser.Name) 233 | assert.Equal(t, "updated@example.com", dbUser.Email) 234 | }) 235 | 236 | // Cleanup 237 | testutil.CleanDB(db) 238 | } 239 | 240 | func TestUserRepository_Delete(t *testing.T) { 241 | // Setup 242 | db, err := testutil.SetupTestDB() 243 | require.NoError(t, err) 244 | logger := testutil.NewTestLogger(t) 245 | repo := NewUserRepository(db, logger) 246 | 247 | t.Run("should delete user successfully", func(t *testing.T) { 248 | // Given 249 | user := testutil.CreateUserFixture() 250 | user.ID = 0 251 | err := repo.Create(user) 252 | require.NoError(t, err) 253 | 254 | // When 255 | err = repo.Delete(user.ID) 256 | 257 | // Then 258 | assert.NoError(t, err) 259 | 260 | // Verify user is deleted 261 | var dbUser entity.User 262 | err = db.First(&dbUser, user.ID).Error 263 | assert.Error(t, err) 264 | assert.Equal(t, gorm.ErrRecordNotFound, err) 265 | }) 266 | 267 | // Cleanup 268 | testutil.CleanDB(db) 269 | } 270 | 271 | func TestUserRepository_EmailExists(t *testing.T) { 272 | // Setup 273 | db, err := testutil.SetupTestDB() 274 | require.NoError(t, err) 275 | logger := testutil.NewTestLogger(t) 276 | repo := NewUserRepository(db, logger) 277 | 278 | t.Run("should return true for existing email", func(t *testing.T) { 279 | // Given 280 | user := testutil.CreateUserFixture() 281 | user.ID = 0 282 | err := repo.Create(user) 283 | require.NoError(t, err) 284 | 285 | // When 286 | exists, err := repo.EmailExists(user.Email) 287 | 288 | // Then 289 | assert.NoError(t, err) 290 | assert.True(t, exists) 291 | }) 292 | 293 | t.Run("should return false for non-existing email", func(t *testing.T) { 294 | // When 295 | exists, err := repo.EmailExists("nonexistent@example.com") 296 | 297 | // Then 298 | assert.NoError(t, err) 299 | assert.False(t, exists) 300 | }) 301 | 302 | // Cleanup 303 | testutil.CleanDB(db) 304 | } 305 | -------------------------------------------------------------------------------- /test/integration/user_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "vibe-ddd-golang/internal/application/user/dto" 11 | "vibe-ddd-golang/internal/application/user/handler" 12 | "vibe-ddd-golang/internal/application/user/repository" 13 | "vibe-ddd-golang/internal/application/user/service" 14 | "vibe-ddd-golang/internal/pkg/testutil" 15 | 16 | "github.com/gin-gonic/gin" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func setupUserIntegration(t *testing.T) (*gin.Engine, func()) { 22 | gin.SetMode(gin.TestMode) 23 | 24 | // Setup test database 25 | db, err := testutil.SetupTestDB() 26 | require.NoError(t, err) 27 | 28 | logger := testutil.NewTestLogger(t) 29 | 30 | // Create real instances (no mocks) 31 | userRepo := repository.NewUserRepository(db, logger) 32 | userService := service.NewUserService(userRepo, logger) 33 | userHandler := handler.NewUserHandler(userService, logger) 34 | 35 | // Setup Gin router 36 | router := gin.New() 37 | api := router.Group("/api/v1") 38 | userHandler.RegisterRoutes(api) 39 | 40 | cleanup := func() { 41 | testutil.CleanDB(db) 42 | } 43 | 44 | return router, cleanup 45 | } 46 | 47 | func TestUserIntegration_CreateAndGetUser(t *testing.T) { 48 | router, cleanup := setupUserIntegration(t) 49 | defer cleanup() 50 | 51 | // Test data 52 | createReq := &dto.CreateUserRequest{ 53 | Name: "John Doe", 54 | Email: "john@example.com", 55 | Password: "password123", 56 | } 57 | 58 | // Step 1: Create user 59 | reqBody, _ := json.Marshal(createReq) 60 | w := httptest.NewRecorder() 61 | req := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody)) 62 | req.Header.Set("Content-Type", "application/json") 63 | 64 | router.ServeHTTP(w, req) 65 | 66 | assert.Equal(t, http.StatusCreated, w.Code) 67 | 68 | var createResp map[string]interface{} 69 | err := json.Unmarshal(w.Body.Bytes(), &createResp) 70 | require.NoError(t, err) 71 | 72 | data := createResp["data"].(map[string]interface{}) 73 | userID := int(data["id"].(float64)) 74 | assert.Equal(t, createReq.Name, data["name"]) 75 | assert.Equal(t, createReq.Email, data["email"]) 76 | 77 | // Step 2: Get the created user 78 | w2 := httptest.NewRecorder() 79 | req2 := httptest.NewRequest("GET", "/api/v1/users/"+string(rune(userID+'0')), nil) 80 | 81 | router.ServeHTTP(w2, req2) 82 | 83 | assert.Equal(t, http.StatusOK, w2.Code) 84 | 85 | var getResp map[string]interface{} 86 | err = json.Unmarshal(w2.Body.Bytes(), &getResp) 87 | require.NoError(t, err) 88 | 89 | userData := getResp["data"].(map[string]interface{}) 90 | assert.Equal(t, float64(userID), userData["id"]) 91 | assert.Equal(t, createReq.Name, userData["name"]) 92 | assert.Equal(t, createReq.Email, userData["email"]) 93 | } 94 | 95 | func TestUserIntegration_CreateDuplicateEmail(t *testing.T) { 96 | router, cleanup := setupUserIntegration(t) 97 | defer cleanup() 98 | 99 | // Test data 100 | createReq := &dto.CreateUserRequest{ 101 | Name: "John Doe", 102 | Email: "duplicate@example.com", 103 | Password: "password123", 104 | } 105 | 106 | // Step 1: Create first user 107 | reqBody, _ := json.Marshal(createReq) 108 | w1 := httptest.NewRecorder() 109 | req1 := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody)) 110 | req1.Header.Set("Content-Type", "application/json") 111 | 112 | router.ServeHTTP(w1, req1) 113 | assert.Equal(t, http.StatusCreated, w1.Code) 114 | 115 | // Step 2: Try to create user with same email 116 | w2 := httptest.NewRecorder() 117 | req2 := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody)) 118 | req2.Header.Set("Content-Type", "application/json") 119 | 120 | router.ServeHTTP(w2, req2) 121 | assert.Equal(t, http.StatusConflict, w2.Code) 122 | 123 | var errorResp map[string]interface{} 124 | err := json.Unmarshal(w2.Body.Bytes(), &errorResp) 125 | require.NoError(t, err) 126 | assert.Contains(t, errorResp["error"], "email already exists") 127 | } 128 | 129 | func TestUserIntegration_GetUsers(t *testing.T) { 130 | router, cleanup := setupUserIntegration(t) 131 | defer cleanup() 132 | 133 | // Create multiple users 134 | users := []dto.CreateUserRequest{ 135 | {Name: "User 1", Email: "user1@example.com", Password: "password1"}, 136 | {Name: "User 2", Email: "user2@example.com", Password: "password2"}, 137 | {Name: "User 3", Email: "user3@example.com", Password: "password3"}, 138 | } 139 | 140 | // Create users 141 | for _, user := range users { 142 | reqBody, _ := json.Marshal(user) 143 | w := httptest.NewRecorder() 144 | req := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody)) 145 | req.Header.Set("Content-Type", "application/json") 146 | 147 | router.ServeHTTP(w, req) 148 | assert.Equal(t, http.StatusCreated, w.Code) 149 | } 150 | 151 | // Get all users 152 | w := httptest.NewRecorder() 153 | req := httptest.NewRequest("GET", "/api/v1/users?page=1&page_size=10", nil) 154 | 155 | router.ServeHTTP(w, req) 156 | 157 | assert.Equal(t, http.StatusOK, w.Code) 158 | 159 | var response dto.UserListResponse 160 | err := json.Unmarshal(w.Body.Bytes(), &response) 161 | require.NoError(t, err) 162 | 163 | assert.Len(t, response.Data, 3) 164 | assert.Equal(t, int64(3), response.TotalCount) 165 | assert.Equal(t, 1, response.Page) 166 | assert.Equal(t, 10, response.PageSize) 167 | } 168 | 169 | func TestUserIntegration_UpdateUser(t *testing.T) { 170 | router, cleanup := setupUserIntegration(t) 171 | defer cleanup() 172 | 173 | // Create user 174 | createReq := &dto.CreateUserRequest{ 175 | Name: "Original Name", 176 | Email: "original@example.com", 177 | Password: "password123", 178 | } 179 | 180 | reqBody, _ := json.Marshal(createReq) 181 | w := httptest.NewRecorder() 182 | req := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody)) 183 | req.Header.Set("Content-Type", "application/json") 184 | 185 | router.ServeHTTP(w, req) 186 | assert.Equal(t, http.StatusCreated, w.Code) 187 | 188 | var createResp map[string]interface{} 189 | err := json.Unmarshal(w.Body.Bytes(), &createResp) 190 | require.NoError(t, err) 191 | 192 | data := createResp["data"].(map[string]interface{}) 193 | userID := int(data["id"].(float64)) 194 | 195 | // Update user 196 | updateReq := &dto.UpdateUserRequest{ 197 | Name: "Updated Name", 198 | Email: "updated@example.com", 199 | } 200 | 201 | updateBody, _ := json.Marshal(updateReq) 202 | w2 := httptest.NewRecorder() 203 | req2 := httptest.NewRequest("PUT", "/api/v1/users/"+string(rune(userID+'0')), bytes.NewBuffer(updateBody)) 204 | req2.Header.Set("Content-Type", "application/json") 205 | 206 | router.ServeHTTP(w2, req2) 207 | 208 | assert.Equal(t, http.StatusOK, w2.Code) 209 | 210 | var updateResp map[string]interface{} 211 | err = json.Unmarshal(w2.Body.Bytes(), &updateResp) 212 | require.NoError(t, err) 213 | 214 | updatedData := updateResp["data"].(map[string]interface{}) 215 | assert.Equal(t, updateReq.Name, updatedData["name"]) 216 | assert.Equal(t, updateReq.Email, updatedData["email"]) 217 | } 218 | 219 | func TestUserIntegration_DeleteUser(t *testing.T) { 220 | router, cleanup := setupUserIntegration(t) 221 | defer cleanup() 222 | 223 | // Create user 224 | createReq := &dto.CreateUserRequest{ 225 | Name: "To Be Deleted", 226 | Email: "delete@example.com", 227 | Password: "password123", 228 | } 229 | 230 | reqBody, _ := json.Marshal(createReq) 231 | w := httptest.NewRecorder() 232 | req := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(reqBody)) 233 | req.Header.Set("Content-Type", "application/json") 234 | 235 | router.ServeHTTP(w, req) 236 | assert.Equal(t, http.StatusCreated, w.Code) 237 | 238 | var createResp map[string]interface{} 239 | err := json.Unmarshal(w.Body.Bytes(), &createResp) 240 | require.NoError(t, err) 241 | 242 | data := createResp["data"].(map[string]interface{}) 243 | userID := int(data["id"].(float64)) 244 | 245 | // Delete user 246 | w2 := httptest.NewRecorder() 247 | req2 := httptest.NewRequest("DELETE", "/api/v1/users/"+string(rune(userID+'0')), nil) 248 | 249 | router.ServeHTTP(w2, req2) 250 | 251 | assert.Equal(t, http.StatusOK, w2.Code) 252 | 253 | var deleteResp map[string]interface{} 254 | err = json.Unmarshal(w2.Body.Bytes(), &deleteResp) 255 | require.NoError(t, err) 256 | assert.Equal(t, "User deleted successfully", deleteResp["message"]) 257 | 258 | // Try to get deleted user (should return 404) 259 | w3 := httptest.NewRecorder() 260 | req3 := httptest.NewRequest("GET", "/api/v1/users/"+string(rune(userID+'0')), nil) 261 | 262 | router.ServeHTTP(w3, req3) 263 | assert.Equal(t, http.StatusNotFound, w3.Code) 264 | } 265 | -------------------------------------------------------------------------------- /internal/application/payment/worker/handler.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "vibe-ddd-golang/internal/application/payment/dto" 10 | "vibe-ddd-golang/internal/application/payment/entity" 11 | "vibe-ddd-golang/internal/application/payment/service" 12 | "vibe-ddd-golang/internal/config" 13 | 14 | "github.com/hibiken/asynq" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | type AsynqClient interface { 19 | Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error) 20 | } 21 | 22 | type PaymentWorker struct { 23 | paymentService service.PaymentService 24 | client AsynqClient 25 | logger *zap.Logger 26 | cfg *config.Config 27 | } 28 | 29 | type CheckPaymentStatusPayload struct { 30 | PaymentID uint `json:"payment_id"` 31 | } 32 | 33 | type ProcessPaymentPayload struct { 34 | PaymentID uint `json:"payment_id"` 35 | } 36 | 37 | func NewPaymentWorker( 38 | paymentService service.PaymentService, 39 | client AsynqClient, 40 | logger *zap.Logger, 41 | cfg *config.Config, 42 | ) *PaymentWorker { 43 | return &PaymentWorker{ 44 | paymentService: paymentService, 45 | client: client, 46 | logger: logger, 47 | cfg: cfg, 48 | } 49 | } 50 | 51 | func (w *PaymentWorker) HandleCheckPaymentStatus(ctx context.Context, task *asynq.Task) error { 52 | var payload CheckPaymentStatusPayload 53 | if err := json.Unmarshal(task.Payload(), &payload); err != nil { 54 | w.logger.Error("Failed to unmarshal payment status check payload", 55 | zap.Error(err), 56 | zap.ByteString("payload", task.Payload())) 57 | return fmt.Errorf("json.Unmarshal failed: %w", err) 58 | } 59 | 60 | w.logger.Info("Processing payment status check", 61 | zap.Uint("payment_id", payload.PaymentID)) 62 | 63 | // Get payment from database 64 | payment, err := w.paymentService.GetPaymentByID(payload.PaymentID) 65 | if err != nil { 66 | w.logger.Error("Failed to get payment", 67 | zap.Uint("payment_id", payload.PaymentID), 68 | zap.Error(err)) 69 | return fmt.Errorf("failed to get payment: %w", err) 70 | } 71 | 72 | // Skip if payment is already completed or failed 73 | if payment.Status == entity.PaymentStatusCompleted.String() || 74 | payment.Status == entity.PaymentStatusFailed.String() || 75 | payment.Status == entity.PaymentStatusCanceled.String() { 76 | w.logger.Info("Payment already in final state, skipping check", 77 | zap.Uint("payment_id", payload.PaymentID), 78 | zap.String("status", payment.Status)) 79 | return nil 80 | } 81 | 82 | // Simulate external payment gateway status check 83 | // In real implementation, you would call external payment gateway API 84 | newStatus := w.simulatePaymentGatewayCheck(payment) 85 | 86 | // Update payment status if changed 87 | if newStatus != payment.Status { 88 | updateReq := &dto.UpdatePaymentRequest{ 89 | Status: newStatus, 90 | Description: fmt.Sprintf("Status updated by worker at %s", time.Now().Format(time.RFC3339)), 91 | } 92 | 93 | _, err := w.paymentService.UpdatePayment(payload.PaymentID, updateReq) 94 | if err != nil { 95 | w.logger.Error("Failed to update payment status", 96 | zap.Uint("payment_id", payload.PaymentID), 97 | zap.String("new_status", newStatus), 98 | zap.Error(err)) 99 | return fmt.Errorf("failed to update payment status: %w", err) 100 | } 101 | 102 | w.logger.Info("Payment status updated", 103 | zap.Uint("payment_id", payload.PaymentID), 104 | zap.String("old_status", payment.Status), 105 | zap.String("new_status", newStatus)) 106 | } 107 | 108 | // Schedule next check if payment is still pending 109 | if newStatus == entity.PaymentStatusPending.String() { 110 | if err := w.SchedulePaymentStatusCheck(payload.PaymentID, w.cfg.Worker.PaymentCheckInterval); err != nil { 111 | w.logger.Error("Failed to schedule next payment check", 112 | zap.Uint("payment_id", payload.PaymentID), 113 | zap.Error(err)) 114 | // Don't return error as the current task was successful 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (w *PaymentWorker) HandleProcessPayment(ctx context.Context, task *asynq.Task) error { 122 | var payload ProcessPaymentPayload 123 | if err := json.Unmarshal(task.Payload(), &payload); err != nil { 124 | w.logger.Error("Failed to unmarshal process payment payload", 125 | zap.Error(err), 126 | zap.ByteString("payload", task.Payload())) 127 | return fmt.Errorf("json.Unmarshal failed: %w", err) 128 | } 129 | 130 | w.logger.Info("Processing payment", 131 | zap.Uint("payment_id", payload.PaymentID)) 132 | 133 | // Get payment from database 134 | payment, err := w.paymentService.GetPaymentByID(payload.PaymentID) 135 | if err != nil { 136 | w.logger.Error("Failed to get payment for processing", 137 | zap.Uint("payment_id", payload.PaymentID), 138 | zap.Error(err)) 139 | return fmt.Errorf("failed to get payment: %w", err) 140 | } 141 | 142 | // Simulate payment processing 143 | // In real implementation, you would call external payment gateway 144 | success := w.simulatePaymentProcessing(payment) 145 | 146 | var newStatus string 147 | if success { 148 | newStatus = entity.PaymentStatusCompleted.String() 149 | } else { 150 | newStatus = entity.PaymentStatusFailed.String() 151 | } 152 | 153 | updateReq := &dto.UpdatePaymentRequest{ 154 | Status: newStatus, 155 | Description: fmt.Sprintf("Payment processed by worker at %s", time.Now().Format(time.RFC3339)), 156 | } 157 | 158 | _, err = w.paymentService.UpdatePayment(payload.PaymentID, updateReq) 159 | if err != nil { 160 | w.logger.Error("Failed to update payment after processing", 161 | zap.Uint("payment_id", payload.PaymentID), 162 | zap.String("new_status", newStatus), 163 | zap.Error(err)) 164 | return fmt.Errorf("failed to update payment: %w", err) 165 | } 166 | 167 | w.logger.Info("Payment processing completed", 168 | zap.Uint("payment_id", payload.PaymentID), 169 | zap.String("final_status", newStatus), 170 | zap.Bool("success", success)) 171 | 172 | return nil 173 | } 174 | 175 | func (w *PaymentWorker) SchedulePaymentStatusCheck(paymentID uint, delay time.Duration) error { 176 | payload := CheckPaymentStatusPayload{PaymentID: paymentID} 177 | payloadBytes, err := json.Marshal(payload) 178 | if err != nil { 179 | return fmt.Errorf("failed to marshal payload: %w", err) 180 | } 181 | 182 | task := asynq.NewTask(TypeCheckPaymentStatus, payloadBytes) 183 | opts := []asynq.Option{ 184 | asynq.ProcessIn(delay), 185 | asynq.Queue("default"), 186 | asynq.MaxRetry(w.cfg.Worker.RetryMaxAttempts), 187 | } 188 | 189 | info, err := w.client.Enqueue(task, opts...) 190 | if err != nil { 191 | return fmt.Errorf("failed to enqueue task: %w", err) 192 | } 193 | 194 | w.logger.Info("Scheduled payment status check", 195 | zap.Uint("payment_id", paymentID), 196 | zap.Duration("delay", delay), 197 | zap.String("task_id", info.ID)) 198 | 199 | return nil 200 | } 201 | 202 | func (w *PaymentWorker) SchedulePaymentProcessing(paymentID uint) error { 203 | payload := ProcessPaymentPayload{PaymentID: paymentID} 204 | payloadBytes, err := json.Marshal(payload) 205 | if err != nil { 206 | return fmt.Errorf("failed to marshal payload: %w", err) 207 | } 208 | 209 | task := asynq.NewTask(TypeProcessPayment, payloadBytes) 210 | opts := []asynq.Option{ 211 | asynq.Queue("critical"), 212 | asynq.MaxRetry(w.cfg.Worker.RetryMaxAttempts), 213 | } 214 | 215 | info, err := w.client.Enqueue(task, opts...) 216 | if err != nil { 217 | return fmt.Errorf("failed to enqueue task: %w", err) 218 | } 219 | 220 | w.logger.Info("Scheduled payment processing", 221 | zap.Uint("payment_id", paymentID), 222 | zap.String("task_id", info.ID)) 223 | 224 | return nil 225 | } 226 | 227 | // simulatePaymentGatewayCheck simulates checking payment status with external gateway 228 | func (w *PaymentWorker) simulatePaymentGatewayCheck(payment *dto.PaymentResponse) string { 229 | // Simulate random status changes for demo purposes 230 | // In real implementation, this would call actual payment gateway API 231 | 232 | elapsed := time.Since(payment.CreatedAt) 233 | 234 | // After 2 minutes, 80% chance to complete, 10% to fail, 10% stay pending 235 | if elapsed > 2*time.Minute { 236 | rand := time.Now().UnixNano() % 10 237 | if rand < 8 { 238 | return entity.PaymentStatusCompleted.String() 239 | } else if rand < 9 { 240 | return entity.PaymentStatusFailed.String() 241 | } 242 | } 243 | 244 | return entity.PaymentStatusPending.String() 245 | } 246 | 247 | // simulatePaymentProcessing simulates processing payment with external gateway 248 | func (w *PaymentWorker) simulatePaymentProcessing(payment *dto.PaymentResponse) bool { 249 | // Simulate 90% success rate for demo purposes 250 | rand := time.Now().UnixNano() % 10 251 | return rand < 9 252 | } 253 | -------------------------------------------------------------------------------- /internal/application/user/handler/user.handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "vibe-ddd-golang/internal/application/user/dto" 8 | "vibe-ddd-golang/internal/application/user/service" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type UserHandler struct { 15 | service service.UserService 16 | logger *zap.Logger 17 | } 18 | 19 | func NewUserHandler(service service.UserService, logger *zap.Logger) *UserHandler { 20 | return &UserHandler{ 21 | service: service, 22 | logger: logger, 23 | } 24 | } 25 | 26 | // CreateUser godoc 27 | // @Summary Create a new user 28 | // @Description Create a new user with the provided information 29 | // @Tags users 30 | // @Accept json 31 | // @Produce json 32 | // @Param user body dto.CreateUserRequest true "User creation request" 33 | // @Success 201 {object} map[string]interface{} "Created user" 34 | // @Failure 400 {object} map[string]interface{} "Invalid request body" 35 | // @Failure 409 {object} map[string]interface{} "Email already exists" 36 | // @Failure 500 {object} map[string]interface{} "Internal server error" 37 | // @Router /users [post] 38 | func (h *UserHandler) CreateUser(ctx *gin.Context) { 39 | var req dto.CreateUserRequest 40 | if err := ctx.ShouldBindJSON(&req); err != nil { 41 | h.logger.Error("Invalid request body", zap.Error(err)) 42 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | 46 | user, err := h.service.CreateUser(&req) 47 | if err != nil { 48 | h.logger.Error("Failed to create user", zap.Error(err)) 49 | if err.Error() == "email already exists" { 50 | ctx.JSON(http.StatusConflict, gin.H{"error": err.Error()}) 51 | return 52 | } 53 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) 54 | return 55 | } 56 | 57 | ctx.JSON(http.StatusCreated, gin.H{"data": user}) 58 | } 59 | 60 | // GetUser godoc 61 | // @Summary Get a user by ID 62 | // @Description Get a single user by their ID 63 | // @Tags users 64 | // @Accept json 65 | // @Produce json 66 | // @Param id path int true "User ID" 67 | // @Success 200 {object} map[string]interface{} "User details" 68 | // @Failure 400 {object} map[string]interface{} "Invalid user ID" 69 | // @Failure 404 {object} map[string]interface{} "User not found" 70 | // @Router /users/{id} [get] 71 | func (h *UserHandler) GetUser(ctx *gin.Context) { 72 | idStr := ctx.Param("id") 73 | id, err := strconv.ParseUint(idStr, 10, 32) 74 | if err != nil { 75 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) 76 | return 77 | } 78 | 79 | user, err := h.service.GetUserByID(uint(id)) 80 | if err != nil { 81 | h.logger.Error("Failed to get user", zap.Error(err)) 82 | ctx.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) 83 | return 84 | } 85 | 86 | ctx.JSON(http.StatusOK, gin.H{"data": user}) 87 | } 88 | 89 | // GetUsers godoc 90 | // @Summary Get all users 91 | // @Description Get a list of users with optional filtering and pagination 92 | // @Tags users 93 | // @Accept json 94 | // @Produce json 95 | // @Param name query string false "Filter by name" 96 | // @Param email query string false "Filter by email" 97 | // @Param page query int false "Page number" default(1) 98 | // @Param page_size query int false "Number of items per page" default(10) 99 | // @Success 200 {object} dto.UserListResponse "List of users" 100 | // @Failure 400 {object} map[string]interface{} "Invalid query parameters" 101 | // @Failure 500 {object} map[string]interface{} "Internal server error" 102 | // @Router /users [get] 103 | func (h *UserHandler) GetUsers(ctx *gin.Context) { 104 | var filter dto.UserFilter 105 | if err := ctx.ShouldBindQuery(&filter); err != nil { 106 | h.logger.Error("Invalid query parameters", zap.Error(err)) 107 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 108 | return 109 | } 110 | 111 | users, err := h.service.GetUsers(&filter) 112 | if err != nil { 113 | h.logger.Error("Failed to get users", zap.Error(err)) 114 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users"}) 115 | return 116 | } 117 | 118 | ctx.JSON(http.StatusOK, users) 119 | } 120 | 121 | // UpdateUser godoc 122 | // @Summary Update a user 123 | // @Description Update a user's information by ID 124 | // @Tags users 125 | // @Accept json 126 | // @Produce json 127 | // @Param id path int true "User ID" 128 | // @Param user body dto.UpdateUserRequest true "User update request" 129 | // @Success 200 {object} map[string]interface{} "Updated user" 130 | // @Failure 400 {object} map[string]interface{} "Invalid request" 131 | // @Failure 404 {object} map[string]interface{} "User not found" 132 | // @Failure 409 {object} map[string]interface{} "Email already exists" 133 | // @Failure 500 {object} map[string]interface{} "Internal server error" 134 | // @Router /users/{id} [put] 135 | func (h *UserHandler) UpdateUser(ctx *gin.Context) { 136 | idStr := ctx.Param("id") 137 | id, err := strconv.ParseUint(idStr, 10, 32) 138 | if err != nil { 139 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) 140 | return 141 | } 142 | 143 | var req dto.UpdateUserRequest 144 | if err := ctx.ShouldBindJSON(&req); err != nil { 145 | h.logger.Error("Invalid request body", zap.Error(err)) 146 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 147 | return 148 | } 149 | 150 | user, err := h.service.UpdateUser(uint(id), &req) 151 | if err != nil { 152 | h.logger.Error("Failed to update user", zap.Error(err)) 153 | if err.Error() == "user not found" { 154 | ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 155 | return 156 | } 157 | if err.Error() == "email already exists" { 158 | ctx.JSON(http.StatusConflict, gin.H{"error": err.Error()}) 159 | return 160 | } 161 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) 162 | return 163 | } 164 | 165 | ctx.JSON(http.StatusOK, gin.H{"data": user}) 166 | } 167 | 168 | // UpdateUserPassword godoc 169 | // @Summary Update user password 170 | // @Description Update a user's password by ID 171 | // @Tags users 172 | // @Accept json 173 | // @Produce json 174 | // @Param id path int true "User ID" 175 | // @Param password body dto.UpdateUserPasswordRequest true "Password update request" 176 | // @Success 200 {object} map[string]interface{} "Password updated successfully" 177 | // @Failure 400 {object} map[string]interface{} "Invalid request" 178 | // @Failure 401 {object} map[string]interface{} "Current password is incorrect" 179 | // @Failure 404 {object} map[string]interface{} "User not found" 180 | // @Failure 500 {object} map[string]interface{} "Internal server error" 181 | // @Router /users/{id}/password [put] 182 | func (h *UserHandler) UpdateUserPassword(ctx *gin.Context) { 183 | idStr := ctx.Param("id") 184 | id, err := strconv.ParseUint(idStr, 10, 32) 185 | if err != nil { 186 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) 187 | return 188 | } 189 | 190 | var req dto.UpdateUserPasswordRequest 191 | if err := ctx.ShouldBindJSON(&req); err != nil { 192 | h.logger.Error("Invalid request body", zap.Error(err)) 193 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 194 | return 195 | } 196 | 197 | err = h.service.UpdateUserPassword(uint(id), &req) 198 | if err != nil { 199 | h.logger.Error("Failed to update user password", zap.Error(err)) 200 | if err.Error() == "user not found" { 201 | ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 202 | return 203 | } 204 | if err.Error() == "current password is incorrect" { 205 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 206 | return 207 | } 208 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) 209 | return 210 | } 211 | 212 | ctx.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) 213 | } 214 | 215 | // DeleteUser godoc 216 | // @Summary Delete a user 217 | // @Description Delete a user by ID 218 | // @Tags users 219 | // @Accept json 220 | // @Produce json 221 | // @Param id path int true "User ID" 222 | // @Success 200 {object} map[string]interface{} "User deleted successfully" 223 | // @Failure 400 {object} map[string]interface{} "Invalid user ID" 224 | // @Failure 404 {object} map[string]interface{} "User not found" 225 | // @Failure 500 {object} map[string]interface{} "Internal server error" 226 | // @Router /users/{id} [delete] 227 | func (h *UserHandler) DeleteUser(ctx *gin.Context) { 228 | idStr := ctx.Param("id") 229 | id, err := strconv.ParseUint(idStr, 10, 32) 230 | if err != nil { 231 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) 232 | return 233 | } 234 | 235 | err = h.service.DeleteUser(uint(id)) 236 | if err != nil { 237 | h.logger.Error("Failed to delete user", zap.Error(err)) 238 | if err.Error() == "user not found" { 239 | ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 240 | return 241 | } 242 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) 243 | return 244 | } 245 | 246 | ctx.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) 247 | } 248 | 249 | func (h *UserHandler) RegisterRoutes(api *gin.RouterGroup) { 250 | users := api.Group("/users") 251 | { 252 | users.POST("", h.CreateUser) 253 | users.GET("", h.GetUsers) 254 | users.GET("/:id", h.GetUser) 255 | users.PUT("/:id", h.UpdateUser) 256 | users.DELETE("/:id", h.DeleteUser) 257 | users.PUT("/:id/password", h.UpdateUserPassword) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /internal/application/payment/repository/payment.repo_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "testing" 5 | 6 | "vibe-ddd-golang/internal/application/payment/dto" 7 | "vibe-ddd-golang/internal/application/payment/entity" 8 | "vibe-ddd-golang/internal/pkg/testutil" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func TestPaymentRepository_Create(t *testing.T) { 16 | // Setup 17 | db, err := testutil.SetupTestDB() 18 | require.NoError(t, err) 19 | logger := testutil.NewTestLogger(t) 20 | repo := NewPaymentRepository(db, logger) 21 | 22 | t.Run("should create payment successfully", func(t *testing.T) { 23 | // Given 24 | payment := testutil.CreatePaymentFixture() 25 | payment.ID = 0 // Reset ID for creation 26 | 27 | // When 28 | err := repo.Create(payment) 29 | 30 | // Then 31 | assert.NoError(t, err) 32 | assert.NotZero(t, payment.ID) 33 | 34 | // Verify payment was created in database 35 | var dbPayment entity.Payment 36 | err = db.First(&dbPayment, payment.ID).Error 37 | assert.NoError(t, err) 38 | assert.Equal(t, payment.Amount, dbPayment.Amount) 39 | assert.Equal(t, payment.Currency, dbPayment.Currency) 40 | assert.Equal(t, payment.UserID, dbPayment.UserID) 41 | }) 42 | 43 | // Cleanup 44 | testutil.CleanDB(db) 45 | } 46 | 47 | func TestPaymentRepository_GetByID(t *testing.T) { 48 | // Setup 49 | db, err := testutil.SetupTestDB() 50 | require.NoError(t, err) 51 | logger := testutil.NewTestLogger(t) 52 | repo := NewPaymentRepository(db, logger) 53 | 54 | t.Run("should get payment by ID successfully", func(t *testing.T) { 55 | // Given 56 | payment := testutil.CreatePaymentFixture() 57 | payment.ID = 0 58 | err := repo.Create(payment) 59 | require.NoError(t, err) 60 | 61 | // When 62 | foundPayment, err := repo.GetByID(payment.ID) 63 | 64 | // Then 65 | assert.NoError(t, err) 66 | assert.Equal(t, payment.ID, foundPayment.ID) 67 | assert.Equal(t, payment.Amount, foundPayment.Amount) 68 | assert.Equal(t, payment.Currency, foundPayment.Currency) 69 | assert.Equal(t, payment.UserID, foundPayment.UserID) 70 | }) 71 | 72 | t.Run("should return error when payment not found", func(t *testing.T) { 73 | // When 74 | _, err := repo.GetByID(999) 75 | 76 | // Then 77 | assert.Error(t, err) 78 | assert.Equal(t, gorm.ErrRecordNotFound, err) 79 | }) 80 | 81 | // Cleanup 82 | testutil.CleanDB(db) 83 | } 84 | 85 | func TestPaymentRepository_GetAll(t *testing.T) { 86 | // Setup 87 | db, err := testutil.SetupTestDB() 88 | require.NoError(t, err) 89 | logger := testutil.NewTestLogger(t) 90 | repo := NewPaymentRepository(db, logger) 91 | 92 | // Clean up function 93 | cleanup := func() { 94 | db.Exec("DELETE FROM payments") 95 | } 96 | 97 | t.Run("should get all payments with pagination", func(t *testing.T) { 98 | cleanup() // Clean before test 99 | // Given - Create multiple payments 100 | for i := 0; i < 5; i++ { 101 | payment := testutil.CreatePaymentFixture() 102 | payment.ID = 0 103 | payment.Amount = float64(100 + i) 104 | payment.UserID = uint(i + 1) 105 | err := repo.Create(payment) 106 | require.NoError(t, err) 107 | } 108 | 109 | filter := &dto.PaymentFilter{ 110 | Page: 1, 111 | PageSize: 3, 112 | } 113 | 114 | // When 115 | payments, totalCount, err := repo.GetAll(filter) 116 | 117 | // Then 118 | assert.NoError(t, err) 119 | assert.Len(t, payments, 3) // Should return 3 payments due to page size 120 | assert.Equal(t, int64(5), totalCount) // Total count should be 5 121 | }) 122 | 123 | t.Run("should filter payments by status", func(t *testing.T) { 124 | cleanup() // Clean before test 125 | // Given 126 | payment1 := testutil.CreatePaymentFixture() 127 | payment1.ID = 0 128 | payment1.Status = entity.PaymentStatusPending 129 | payment1.UserID = 1 130 | err := repo.Create(payment1) 131 | require.NoError(t, err) 132 | 133 | payment2 := testutil.CreatePaymentFixture() 134 | payment2.ID = 0 135 | payment2.Status = entity.PaymentStatusCompleted 136 | payment2.UserID = 2 137 | err = repo.Create(payment2) 138 | require.NoError(t, err) 139 | 140 | filter := &dto.PaymentFilter{ 141 | Status: entity.PaymentStatusPending.String(), 142 | } 143 | 144 | // When 145 | payments, totalCount, err := repo.GetAll(filter) 146 | 147 | // Then 148 | assert.NoError(t, err) 149 | assert.Len(t, payments, 1) 150 | assert.Equal(t, int64(1), totalCount) 151 | assert.Equal(t, entity.PaymentStatusPending, payments[0].Status) 152 | }) 153 | 154 | t.Run("should filter payments by currency", func(t *testing.T) { 155 | cleanup() // Clean before test 156 | // Given 157 | payment1 := testutil.CreatePaymentFixture() 158 | payment1.ID = 0 159 | payment1.Currency = "USD" 160 | payment1.UserID = 1 161 | err := repo.Create(payment1) 162 | require.NoError(t, err) 163 | 164 | payment2 := testutil.CreatePaymentFixture() 165 | payment2.ID = 0 166 | payment2.Currency = "EUR" 167 | payment2.UserID = 2 168 | err = repo.Create(payment2) 169 | require.NoError(t, err) 170 | 171 | filter := &dto.PaymentFilter{ 172 | Currency: "USD", 173 | } 174 | 175 | // When 176 | payments, totalCount, err := repo.GetAll(filter) 177 | 178 | // Then 179 | assert.NoError(t, err) 180 | assert.Len(t, payments, 1) 181 | assert.Equal(t, int64(1), totalCount) 182 | assert.Equal(t, "USD", payments[0].Currency) 183 | }) 184 | 185 | t.Run("should filter payments by user ID", func(t *testing.T) { 186 | cleanup() // Clean before test 187 | // Given 188 | payment1 := testutil.CreatePaymentFixture() 189 | payment1.ID = 0 190 | payment1.UserID = 1 191 | err := repo.Create(payment1) 192 | require.NoError(t, err) 193 | 194 | payment2 := testutil.CreatePaymentFixture() 195 | payment2.ID = 0 196 | payment2.UserID = 2 197 | err = repo.Create(payment2) 198 | require.NoError(t, err) 199 | 200 | filter := &dto.PaymentFilter{ 201 | UserID: 1, 202 | } 203 | 204 | // When 205 | payments, totalCount, err := repo.GetAll(filter) 206 | 207 | // Then 208 | assert.NoError(t, err) 209 | assert.Len(t, payments, 1) 210 | assert.Equal(t, int64(1), totalCount) 211 | assert.Equal(t, uint(1), payments[0].UserID) 212 | }) 213 | 214 | // Cleanup 215 | testutil.CleanDB(db) 216 | } 217 | 218 | func TestPaymentRepository_Update(t *testing.T) { 219 | // Setup 220 | db, err := testutil.SetupTestDB() 221 | require.NoError(t, err) 222 | logger := testutil.NewTestLogger(t) 223 | repo := NewPaymentRepository(db, logger) 224 | 225 | t.Run("should update payment successfully", func(t *testing.T) { 226 | // Given 227 | payment := testutil.CreatePaymentFixture() 228 | payment.ID = 0 229 | err := repo.Create(payment) 230 | require.NoError(t, err) 231 | 232 | // When 233 | payment.Status = entity.PaymentStatusCompleted 234 | payment.Description = "Updated description" 235 | err = repo.Update(payment) 236 | 237 | // Then 238 | assert.NoError(t, err) 239 | 240 | // Verify update in database 241 | var dbPayment entity.Payment 242 | err = db.First(&dbPayment, payment.ID).Error 243 | assert.NoError(t, err) 244 | assert.Equal(t, entity.PaymentStatusCompleted, dbPayment.Status) 245 | assert.Equal(t, "Updated description", dbPayment.Description) 246 | }) 247 | 248 | // Cleanup 249 | testutil.CleanDB(db) 250 | } 251 | 252 | func TestPaymentRepository_Delete(t *testing.T) { 253 | // Setup 254 | db, err := testutil.SetupTestDB() 255 | require.NoError(t, err) 256 | logger := testutil.NewTestLogger(t) 257 | repo := NewPaymentRepository(db, logger) 258 | 259 | t.Run("should delete payment successfully", func(t *testing.T) { 260 | // Given 261 | payment := testutil.CreatePaymentFixture() 262 | payment.ID = 0 263 | err := repo.Create(payment) 264 | require.NoError(t, err) 265 | 266 | // When 267 | err = repo.Delete(payment.ID) 268 | 269 | // Then 270 | assert.NoError(t, err) 271 | 272 | // Verify payment is deleted (soft delete with GORM) 273 | var dbPayment entity.Payment 274 | err = db.First(&dbPayment, payment.ID).Error 275 | assert.Error(t, err) 276 | assert.Equal(t, gorm.ErrRecordNotFound, err) 277 | }) 278 | 279 | // Cleanup 280 | testutil.CleanDB(db) 281 | } 282 | 283 | func TestPaymentRepository_GetByUserID(t *testing.T) { 284 | // Setup 285 | db, err := testutil.SetupTestDB() 286 | require.NoError(t, err) 287 | logger := testutil.NewTestLogger(t) 288 | repo := NewPaymentRepository(db, logger) 289 | 290 | t.Run("should get payments by user ID successfully", func(t *testing.T) { 291 | // Given 292 | userID := uint(1) 293 | for i := 0; i < 3; i++ { 294 | payment := testutil.CreatePaymentFixture() 295 | payment.ID = 0 296 | payment.UserID = userID 297 | payment.Amount = float64(100 + i) 298 | err := repo.Create(payment) 299 | require.NoError(t, err) 300 | } 301 | 302 | // Create payment for different user 303 | payment := testutil.CreatePaymentFixture() 304 | payment.ID = 0 305 | payment.UserID = 2 306 | err = repo.Create(payment) 307 | require.NoError(t, err) 308 | 309 | // When 310 | payments, err := repo.GetByUserID(userID) 311 | 312 | // Then 313 | assert.NoError(t, err) 314 | assert.Len(t, payments, 3) // Should return only payments for user 1 315 | for _, p := range payments { 316 | assert.Equal(t, userID, p.UserID) 317 | } 318 | }) 319 | 320 | t.Run("should return empty slice for user with no payments", func(t *testing.T) { 321 | // When 322 | payments, err := repo.GetByUserID(999) 323 | 324 | // Then 325 | assert.NoError(t, err) 326 | assert.Empty(t, payments) 327 | }) 328 | 329 | // Cleanup 330 | testutil.CleanDB(db) 331 | } 332 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build run test clean lint format deps docker-build docker-run 2 | 3 | # Go parameters 4 | GOCMD=go 5 | GOBUILD=$(GOCMD) build 6 | GOCLEAN=$(GOCMD) clean 7 | GOTEST=$(GOCMD) test 8 | GOGET=$(GOCMD) get 9 | GOMOD=$(GOCMD) mod 10 | BINARY_NAME=vibe-ddd-golang 11 | BINARY_PATH=./bin/$(BINARY_NAME) 12 | 13 | # Build the API api 14 | build: 15 | $(GOBUILD) -o $(BINARY_PATH) -v ./cmd/api 16 | 17 | # Build the worker api 18 | build-worker: 19 | $(GOBUILD) -o ./bin/worker -v ./cmd/worker 20 | 21 | # Build the migration api 22 | build-migration: 23 | $(GOBUILD) -o ./bin/migration -v ./cmd/migration 24 | 25 | # Build the gRPC api 26 | build-grpc: 27 | $(GOBUILD) -o ./bin/grpc -v ./cmd/grpc 28 | 29 | # Build all servers 30 | build-all: build build-worker build-migration build-grpc 31 | 32 | # Proto generation commands 33 | proto-gen: 34 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --go-grpc_opt=require_unimplemented_servers=false api/proto/user/user.proto 35 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --go-grpc_opt=require_unimplemented_servers=false api/proto/payment/payment.proto 36 | 37 | # Clean generated proto files 38 | proto-clean: 39 | rm -f api/proto/user/user.pb.go api/proto/user/user_grpc.pb.go 40 | rm -f api/proto/payment/payment.pb.go api/proto/payment/payment_grpc.pb.go 41 | 42 | # Install proto tools 43 | proto-tools: 44 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 45 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 46 | 47 | # Swagger/OpenAPI commands 48 | swagger-gen: 49 | swag init -g cmd/api/main.go -o docs 50 | 51 | # Clean generated swagger files 52 | swagger-clean: 53 | rm -rf docs/ 54 | 55 | # Install swagger tools 56 | swagger-tools: 57 | go install github.com/swaggo/swag/cmd/swag@latest 58 | 59 | # Run the API api 60 | run: 61 | $(GOCMD) run ./cmd/api 62 | 63 | # Run the worker api 64 | run-worker: 65 | $(GOCMD) run ./cmd/worker 66 | 67 | # Run database migrations 68 | run-migration: 69 | $(GOCMD) run ./cmd/migration -action=migrate 70 | 71 | # Run database seeding 72 | run-seed: 73 | $(GOCMD) run ./cmd/migration -action=seed 74 | 75 | # Drop database tables 76 | run-drop: 77 | $(GOCMD) run ./cmd/migration -action=drop 78 | 79 | # Run the gRPC api 80 | run-grpc: 81 | $(GOCMD) run ./cmd/grpc -port=9090 82 | 83 | # Run all tests 84 | test: 85 | $(GOTEST) -v -race -timeout 30s ./... 86 | 87 | # Run tests with coverage 88 | test-coverage: 89 | $(GOTEST) -v -race -coverprofile=coverage.out -covermode=atomic ./... 90 | $(GOCMD) tool cover -html=coverage.out -o coverage.html 91 | @echo "Coverage report generated: coverage.html" 92 | 93 | # Run unit tests only (internal packages) 94 | test-unit: 95 | $(GOTEST) -v -timeout 30s ./internal/... 96 | 97 | # Run integration tests only 98 | test-integration: 99 | $(GOTEST) -v -race -timeout 30s ./test/... 100 | 101 | # Run tests for specific layers 102 | test-repo: 103 | $(GOTEST) -v -race -timeout 30s ./internal/application/*/repository/... 104 | 105 | test-service: 106 | $(GOTEST) -v -race -timeout 30s ./internal/application/*/service/... 107 | 108 | test-handler: 109 | $(GOTEST) -v -race -timeout 30s ./internal/application/*/handler/... 110 | 111 | test-worker: 112 | $(GOTEST) -v -race -timeout 30s ./internal/application/payment/worker/... 113 | 114 | # Run tests for specific domains 115 | test-user: 116 | $(GOTEST) -v -race -timeout 30s ./internal/application/user/... 117 | 118 | test-payment: 119 | $(GOTEST) -v -race -timeout 30s ./internal/application/payment/... 120 | 121 | # Run tests with verbose output and no cache 122 | test-verbose: 123 | $(GOTEST) -v -race -count=1 -timeout 30s ./... 124 | 125 | # Clean build artifacts 126 | clean: 127 | $(GOCLEAN) 128 | rm -rf ./bin 129 | rm -f coverage.out coverage.html 130 | 131 | # Install dependencies 132 | deps: 133 | $(GOMOD) download 134 | $(GOMOD) tidy 135 | 136 | # Linting using golangci-lint (includes nil pointer detection) 137 | lint: 138 | golangci-lint run 139 | 140 | # Lint with auto-fix 141 | lint-fix: 142 | golangci-lint run --fix 143 | 144 | # Lint verbose output 145 | lint-verbose: 146 | golangci-lint run --verbose 147 | 148 | # Show which nil detection linters are enabled 149 | lint-nil-info: 150 | @echo "Nil detection linters enabled:" 151 | @echo " - nilerr: Finds code that returns nil even if it checks that error is not nil" 152 | @echo " - nilnil: Checks that there is no simultaneous return of nil error and invalid value" 153 | @echo "" 154 | @echo "Run 'make lint' to detect potential nil pointer issues" 155 | 156 | # Run specific linter 157 | lint-linter: 158 | @if [ -z "$(LINTER)" ]; then \ 159 | echo "Usage: make lint-linter LINTER=errcheck"; \ 160 | exit 1; \ 161 | fi 162 | golangci-lint run --disable-all --enable=$(LINTER) 163 | 164 | # Lint only new/changed files 165 | lint-new: 166 | golangci-lint run --new-from-rev=HEAD~1 167 | 168 | # Format the code 169 | format: 170 | $(GOCMD) fmt ./... 171 | gofumpt -l -w . 172 | goimports -w . 173 | 174 | # Format with gofumpt (stricter formatting) 175 | format-strict: 176 | gofumpt -l -w . 177 | goimports -w . 178 | gci write --skip-generated -s standard -s default -s "prefix(vibe-ddd-golang)" . 179 | 180 | # Install development tools 181 | tools: 182 | @echo "Installing development tools..." 183 | # Install golangci-lint 184 | @if ! command -v golangci-lint >/dev/null 2>&1; then \ 185 | echo "Installing golangci-lint..."; \ 186 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.54.2; \ 187 | else \ 188 | echo "golangci-lint already installed"; \ 189 | fi 190 | 191 | # Docker build 192 | docker-build: 193 | docker build -t $(BINARY_NAME) . 194 | 195 | # Docker run 196 | docker-run: 197 | docker run -p 8080:8080 $(BINARY_NAME) 198 | 199 | # Development setup 200 | dev-setup: tools deps 201 | @echo "Development environment setup complete" 202 | 203 | # Comprehensive code quality check 204 | quality: 205 | @echo "Running comprehensive code quality checks..." 206 | make format 207 | make lint 208 | make test 209 | @echo "Code quality checks completed!" 210 | 211 | # Pre-commit checks 212 | pre-commit: 213 | @echo "Running pre-commit checks..." 214 | golangci-lint run --fix 215 | make test-unit 216 | @echo "Pre-commit checks passed!" 217 | 218 | # Install pre-commit hooks 219 | install-hooks: 220 | @if [ -f scripts/install-pre-commit.sh ]; then \ 221 | ./scripts/install-pre-commit.sh; \ 222 | else \ 223 | echo "Error: scripts/install-pre-commit.sh not found"; \ 224 | exit 1; \ 225 | fi 226 | 227 | # CI checks (for continuous integration) 228 | ci: 229 | @echo "Running CI checks..." 230 | make lint 231 | make test-coverage 232 | make build-all 233 | @echo "CI checks completed!" 234 | 235 | # Help 236 | help: 237 | @echo "Available targets:" 238 | @echo "" 239 | @echo "Build Commands:" 240 | @echo " build - Build the API server" 241 | @echo " build-worker - Build the worker server" 242 | @echo " build-migration - Build the migration server" 243 | @echo " build-grpc - Build the gRPC server" 244 | @echo " build-all - Build all servers" 245 | @echo "" 246 | @echo "Run Commands:" 247 | @echo " run - Run the API server" 248 | @echo " run-worker - Run the worker server" 249 | @echo " run-migration - Run database migrations" 250 | @echo " run-seed - Run database seeding" 251 | @echo " run-drop - Drop database tables" 252 | @echo " run-grpc - Run the gRPC server" 253 | @echo "" 254 | @echo "Test Commands:" 255 | @echo " test - Run all tests" 256 | @echo " test-coverage - Run tests with coverage" 257 | @echo " test-unit - Run unit tests only" 258 | @echo " test-integration - Run integration tests only" 259 | @echo " test-repo - Run repository layer tests" 260 | @echo " test-service - Run service layer tests" 261 | @echo " test-handler - Run handler layer tests" 262 | @echo " test-worker - Run worker layer tests" 263 | @echo " test-user - Run user domain tests" 264 | @echo " test-payment - Run payment domain tests" 265 | @echo " test-verbose - Run tests with verbose output" 266 | @echo "" 267 | @echo "Development Commands:" 268 | @echo " clean - Clean build artifacts" 269 | @echo " deps - Install dependencies" 270 | @echo " format - Format the code" 271 | @echo " format-strict - Format with stricter rules" 272 | @echo " tools - Install development tools" 273 | @echo " dev-setup - Setup development environment" 274 | @echo "" 275 | @echo "Linting Commands:" 276 | @echo " lint - Run golangci-lint (includes nil detection)" 277 | @echo " lint-fix - Run golangci-lint with auto-fix" 278 | @echo " lint-verbose - Run golangci-lint with verbose output" 279 | @echo " lint-nil-info - Show enabled nil detection linters" 280 | @echo " lint-new - Lint only new/changed code" 281 | @echo " lint-linter - Run specific linter (LINTER=name)" 282 | @echo "" 283 | @echo "Quality Commands:" 284 | @echo " quality - Run comprehensive quality checks" 285 | @echo " pre-commit - Run pre-commit checks" 286 | @echo " install-hooks - Install pre-commit hooks" 287 | @echo " ci - Run CI checks" 288 | @echo "" 289 | @echo "Proto Commands:" 290 | @echo " proto-gen - Generate gRPC code from proto files" 291 | @echo " proto-clean - Clean generated proto files" 292 | @echo " proto-tools - Install proto generation tools" 293 | @echo "" 294 | @echo "Swagger Commands:" 295 | @echo " swagger-gen - Generate Swagger/OpenAPI documentation" 296 | @echo " swagger-clean - Clean generated swagger files" 297 | @echo " swagger-tools - Install swagger generation tools" 298 | @echo "" 299 | @echo "Docker Commands:" 300 | @echo " docker-build - Build Docker image" 301 | @echo " docker-run - Run Docker container" 302 | @echo "" 303 | @echo "Other:" 304 | @echo " help - Show this help" -------------------------------------------------------------------------------- /api/proto/user/user_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v5.29.3 5 | // source: api/proto/user/user.proto 6 | 7 | package user 8 | 9 | import ( 10 | context "context" 11 | 12 | grpc "google.golang.org/grpc" 13 | codes "google.golang.org/grpc/codes" 14 | status "google.golang.org/grpc/status" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | const ( 23 | UserService_CreateUser_FullMethodName = "/user.UserService/CreateUser" 24 | UserService_GetUser_FullMethodName = "/user.UserService/GetUser" 25 | UserService_ListUsers_FullMethodName = "/user.UserService/ListUsers" 26 | UserService_UpdateUser_FullMethodName = "/user.UserService/UpdateUser" 27 | UserService_DeleteUser_FullMethodName = "/user.UserService/DeleteUser" 28 | UserService_UpdateUserPassword_FullMethodName = "/user.UserService/UpdateUserPassword" 29 | ) 30 | 31 | // UserServiceClient is the client API for UserService service. 32 | // 33 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 34 | type UserServiceClient interface { 35 | // Create a new user 36 | CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) 37 | // Get a user by ID 38 | GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) 39 | // List users with pagination 40 | ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) 41 | // Update a user 42 | UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) 43 | // Delete a user 44 | DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) 45 | // Update user password 46 | UpdateUserPassword(ctx context.Context, in *UpdateUserPasswordRequest, opts ...grpc.CallOption) (*UpdateUserPasswordResponse, error) 47 | } 48 | 49 | type userServiceClient struct { 50 | cc grpc.ClientConnInterface 51 | } 52 | 53 | func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { 54 | return &userServiceClient{cc} 55 | } 56 | 57 | func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { 58 | out := new(CreateUserResponse) 59 | err := c.cc.Invoke(ctx, UserService_CreateUser_FullMethodName, in, out, opts...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return out, nil 64 | } 65 | 66 | func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { 67 | out := new(GetUserResponse) 68 | err := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, opts...) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return out, nil 73 | } 74 | 75 | func (c *userServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { 76 | out := new(ListUsersResponse) 77 | err := c.cc.Invoke(ctx, UserService_ListUsers_FullMethodName, in, out, opts...) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return out, nil 82 | } 83 | 84 | func (c *userServiceClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) { 85 | out := new(UpdateUserResponse) 86 | err := c.cc.Invoke(ctx, UserService_UpdateUser_FullMethodName, in, out, opts...) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return out, nil 91 | } 92 | 93 | func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) { 94 | out := new(DeleteUserResponse) 95 | err := c.cc.Invoke(ctx, UserService_DeleteUser_FullMethodName, in, out, opts...) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return out, nil 100 | } 101 | 102 | func (c *userServiceClient) UpdateUserPassword(ctx context.Context, in *UpdateUserPasswordRequest, opts ...grpc.CallOption) (*UpdateUserPasswordResponse, error) { 103 | out := new(UpdateUserPasswordResponse) 104 | err := c.cc.Invoke(ctx, UserService_UpdateUserPassword_FullMethodName, in, out, opts...) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return out, nil 109 | } 110 | 111 | // UserServiceServer is the api API for UserService service. 112 | // All implementations should embed UnimplementedUserServiceServer 113 | // for forward compatibility 114 | type UserServiceServer interface { 115 | // Create a new user 116 | CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) 117 | // Get a user by ID 118 | GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) 119 | // List users with pagination 120 | ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) 121 | // Update a user 122 | UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) 123 | // Delete a user 124 | DeleteUser(context.Context, *DeleteUserRequest) (*DeleteUserResponse, error) 125 | // Update user password 126 | UpdateUserPassword(context.Context, *UpdateUserPasswordRequest) (*UpdateUserPasswordResponse, error) 127 | } 128 | 129 | // UnimplementedUserServiceServer should be embedded to have forward compatible implementations. 130 | type UnimplementedUserServiceServer struct { 131 | } 132 | 133 | func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { 134 | return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") 135 | } 136 | func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) { 137 | return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") 138 | } 139 | func (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { 140 | return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") 141 | } 142 | func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) { 143 | return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") 144 | } 145 | func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*DeleteUserResponse, error) { 146 | return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") 147 | } 148 | func (UnimplementedUserServiceServer) UpdateUserPassword(context.Context, *UpdateUserPasswordRequest) (*UpdateUserPasswordResponse, error) { 149 | return nil, status.Errorf(codes.Unimplemented, "method UpdateUserPassword not implemented") 150 | } 151 | 152 | // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. 153 | // Use of this interface is not recommended, as added methods to UserServiceServer will 154 | // result in compilation errors. 155 | type UnsafeUserServiceServer interface { 156 | mustEmbedUnimplementedUserServiceServer() 157 | } 158 | 159 | func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { 160 | s.RegisterService(&UserService_ServiceDesc, srv) 161 | } 162 | 163 | func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 164 | in := new(CreateUserRequest) 165 | if err := dec(in); err != nil { 166 | return nil, err 167 | } 168 | if interceptor == nil { 169 | return srv.(UserServiceServer).CreateUser(ctx, in) 170 | } 171 | info := &grpc.UnaryServerInfo{ 172 | Server: srv, 173 | FullMethod: UserService_CreateUser_FullMethodName, 174 | } 175 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 176 | return srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) 177 | } 178 | return interceptor(ctx, in, info, handler) 179 | } 180 | 181 | func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 182 | in := new(GetUserRequest) 183 | if err := dec(in); err != nil { 184 | return nil, err 185 | } 186 | if interceptor == nil { 187 | return srv.(UserServiceServer).GetUser(ctx, in) 188 | } 189 | info := &grpc.UnaryServerInfo{ 190 | Server: srv, 191 | FullMethod: UserService_GetUser_FullMethodName, 192 | } 193 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 194 | return srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest)) 195 | } 196 | return interceptor(ctx, in, info, handler) 197 | } 198 | 199 | func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 200 | in := new(ListUsersRequest) 201 | if err := dec(in); err != nil { 202 | return nil, err 203 | } 204 | if interceptor == nil { 205 | return srv.(UserServiceServer).ListUsers(ctx, in) 206 | } 207 | info := &grpc.UnaryServerInfo{ 208 | Server: srv, 209 | FullMethod: UserService_ListUsers_FullMethodName, 210 | } 211 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 212 | return srv.(UserServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) 213 | } 214 | return interceptor(ctx, in, info, handler) 215 | } 216 | 217 | func _UserService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 218 | in := new(UpdateUserRequest) 219 | if err := dec(in); err != nil { 220 | return nil, err 221 | } 222 | if interceptor == nil { 223 | return srv.(UserServiceServer).UpdateUser(ctx, in) 224 | } 225 | info := &grpc.UnaryServerInfo{ 226 | Server: srv, 227 | FullMethod: UserService_UpdateUser_FullMethodName, 228 | } 229 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 230 | return srv.(UserServiceServer).UpdateUser(ctx, req.(*UpdateUserRequest)) 231 | } 232 | return interceptor(ctx, in, info, handler) 233 | } 234 | 235 | func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 236 | in := new(DeleteUserRequest) 237 | if err := dec(in); err != nil { 238 | return nil, err 239 | } 240 | if interceptor == nil { 241 | return srv.(UserServiceServer).DeleteUser(ctx, in) 242 | } 243 | info := &grpc.UnaryServerInfo{ 244 | Server: srv, 245 | FullMethod: UserService_DeleteUser_FullMethodName, 246 | } 247 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 248 | return srv.(UserServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) 249 | } 250 | return interceptor(ctx, in, info, handler) 251 | } 252 | 253 | func _UserService_UpdateUserPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 254 | in := new(UpdateUserPasswordRequest) 255 | if err := dec(in); err != nil { 256 | return nil, err 257 | } 258 | if interceptor == nil { 259 | return srv.(UserServiceServer).UpdateUserPassword(ctx, in) 260 | } 261 | info := &grpc.UnaryServerInfo{ 262 | Server: srv, 263 | FullMethod: UserService_UpdateUserPassword_FullMethodName, 264 | } 265 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 266 | return srv.(UserServiceServer).UpdateUserPassword(ctx, req.(*UpdateUserPasswordRequest)) 267 | } 268 | return interceptor(ctx, in, info, handler) 269 | } 270 | 271 | // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. 272 | // It's only intended for direct use with grpc.RegisterService, 273 | // and not to be introspected or modified (even as a copy) 274 | var UserService_ServiceDesc = grpc.ServiceDesc{ 275 | ServiceName: "user.UserService", 276 | HandlerType: (*UserServiceServer)(nil), 277 | Methods: []grpc.MethodDesc{ 278 | { 279 | MethodName: "CreateUser", 280 | Handler: _UserService_CreateUser_Handler, 281 | }, 282 | { 283 | MethodName: "GetUser", 284 | Handler: _UserService_GetUser_Handler, 285 | }, 286 | { 287 | MethodName: "ListUsers", 288 | Handler: _UserService_ListUsers_Handler, 289 | }, 290 | { 291 | MethodName: "UpdateUser", 292 | Handler: _UserService_UpdateUser_Handler, 293 | }, 294 | { 295 | MethodName: "DeleteUser", 296 | Handler: _UserService_DeleteUser_Handler, 297 | }, 298 | { 299 | MethodName: "UpdateUserPassword", 300 | Handler: _UserService_UpdateUserPassword_Handler, 301 | }, 302 | }, 303 | Streams: []grpc.StreamDesc{}, 304 | Metadata: "api/proto/user/user.proto", 305 | } 306 | -------------------------------------------------------------------------------- /api/proto/payment/payment_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v5.29.3 5 | // source: api/proto/payment/payment.proto 6 | 7 | package payment 8 | 9 | import ( 10 | context "context" 11 | 12 | grpc "google.golang.org/grpc" 13 | codes "google.golang.org/grpc/codes" 14 | status "google.golang.org/grpc/status" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | const ( 23 | PaymentService_CreatePayment_FullMethodName = "/payment.PaymentService/CreatePayment" 24 | PaymentService_GetPayment_FullMethodName = "/payment.PaymentService/GetPayment" 25 | PaymentService_ListPayments_FullMethodName = "/payment.PaymentService/ListPayments" 26 | PaymentService_UpdatePayment_FullMethodName = "/payment.PaymentService/UpdatePayment" 27 | PaymentService_DeletePayment_FullMethodName = "/payment.PaymentService/DeletePayment" 28 | PaymentService_GetUserPayments_FullMethodName = "/payment.PaymentService/GetUserPayments" 29 | ) 30 | 31 | // PaymentServiceClient is the client API for PaymentService service. 32 | // 33 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 34 | type PaymentServiceClient interface { 35 | // Create a new payment 36 | CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error) 37 | // Get a payment by ID 38 | GetPayment(ctx context.Context, in *GetPaymentRequest, opts ...grpc.CallOption) (*GetPaymentResponse, error) 39 | // List payments with filtering 40 | ListPayments(ctx context.Context, in *ListPaymentsRequest, opts ...grpc.CallOption) (*ListPaymentsResponse, error) 41 | // Update a payment 42 | UpdatePayment(ctx context.Context, in *UpdatePaymentRequest, opts ...grpc.CallOption) (*UpdatePaymentResponse, error) 43 | // Delete a payment 44 | DeletePayment(ctx context.Context, in *DeletePaymentRequest, opts ...grpc.CallOption) (*DeletePaymentResponse, error) 45 | // Get payments by user ID 46 | GetUserPayments(ctx context.Context, in *GetUserPaymentsRequest, opts ...grpc.CallOption) (*GetUserPaymentsResponse, error) 47 | } 48 | 49 | type paymentServiceClient struct { 50 | cc grpc.ClientConnInterface 51 | } 52 | 53 | func NewPaymentServiceClient(cc grpc.ClientConnInterface) PaymentServiceClient { 54 | return &paymentServiceClient{cc} 55 | } 56 | 57 | func (c *paymentServiceClient) CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error) { 58 | out := new(CreatePaymentResponse) 59 | err := c.cc.Invoke(ctx, PaymentService_CreatePayment_FullMethodName, in, out, opts...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return out, nil 64 | } 65 | 66 | func (c *paymentServiceClient) GetPayment(ctx context.Context, in *GetPaymentRequest, opts ...grpc.CallOption) (*GetPaymentResponse, error) { 67 | out := new(GetPaymentResponse) 68 | err := c.cc.Invoke(ctx, PaymentService_GetPayment_FullMethodName, in, out, opts...) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return out, nil 73 | } 74 | 75 | func (c *paymentServiceClient) ListPayments(ctx context.Context, in *ListPaymentsRequest, opts ...grpc.CallOption) (*ListPaymentsResponse, error) { 76 | out := new(ListPaymentsResponse) 77 | err := c.cc.Invoke(ctx, PaymentService_ListPayments_FullMethodName, in, out, opts...) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return out, nil 82 | } 83 | 84 | func (c *paymentServiceClient) UpdatePayment(ctx context.Context, in *UpdatePaymentRequest, opts ...grpc.CallOption) (*UpdatePaymentResponse, error) { 85 | out := new(UpdatePaymentResponse) 86 | err := c.cc.Invoke(ctx, PaymentService_UpdatePayment_FullMethodName, in, out, opts...) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return out, nil 91 | } 92 | 93 | func (c *paymentServiceClient) DeletePayment(ctx context.Context, in *DeletePaymentRequest, opts ...grpc.CallOption) (*DeletePaymentResponse, error) { 94 | out := new(DeletePaymentResponse) 95 | err := c.cc.Invoke(ctx, PaymentService_DeletePayment_FullMethodName, in, out, opts...) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return out, nil 100 | } 101 | 102 | func (c *paymentServiceClient) GetUserPayments(ctx context.Context, in *GetUserPaymentsRequest, opts ...grpc.CallOption) (*GetUserPaymentsResponse, error) { 103 | out := new(GetUserPaymentsResponse) 104 | err := c.cc.Invoke(ctx, PaymentService_GetUserPayments_FullMethodName, in, out, opts...) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return out, nil 109 | } 110 | 111 | // PaymentServiceServer is the api API for PaymentService service. 112 | // All implementations should embed UnimplementedPaymentServiceServer 113 | // for forward compatibility 114 | type PaymentServiceServer interface { 115 | // Create a new payment 116 | CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) 117 | // Get a payment by ID 118 | GetPayment(context.Context, *GetPaymentRequest) (*GetPaymentResponse, error) 119 | // List payments with filtering 120 | ListPayments(context.Context, *ListPaymentsRequest) (*ListPaymentsResponse, error) 121 | // Update a payment 122 | UpdatePayment(context.Context, *UpdatePaymentRequest) (*UpdatePaymentResponse, error) 123 | // Delete a payment 124 | DeletePayment(context.Context, *DeletePaymentRequest) (*DeletePaymentResponse, error) 125 | // Get payments by user ID 126 | GetUserPayments(context.Context, *GetUserPaymentsRequest) (*GetUserPaymentsResponse, error) 127 | } 128 | 129 | // UnimplementedPaymentServiceServer should be embedded to have forward compatible implementations. 130 | type UnimplementedPaymentServiceServer struct { 131 | } 132 | 133 | func (UnimplementedPaymentServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) { 134 | return nil, status.Errorf(codes.Unimplemented, "method CreatePayment not implemented") 135 | } 136 | func (UnimplementedPaymentServiceServer) GetPayment(context.Context, *GetPaymentRequest) (*GetPaymentResponse, error) { 137 | return nil, status.Errorf(codes.Unimplemented, "method GetPayment not implemented") 138 | } 139 | func (UnimplementedPaymentServiceServer) ListPayments(context.Context, *ListPaymentsRequest) (*ListPaymentsResponse, error) { 140 | return nil, status.Errorf(codes.Unimplemented, "method ListPayments not implemented") 141 | } 142 | func (UnimplementedPaymentServiceServer) UpdatePayment(context.Context, *UpdatePaymentRequest) (*UpdatePaymentResponse, error) { 143 | return nil, status.Errorf(codes.Unimplemented, "method UpdatePayment not implemented") 144 | } 145 | func (UnimplementedPaymentServiceServer) DeletePayment(context.Context, *DeletePaymentRequest) (*DeletePaymentResponse, error) { 146 | return nil, status.Errorf(codes.Unimplemented, "method DeletePayment not implemented") 147 | } 148 | func (UnimplementedPaymentServiceServer) GetUserPayments(context.Context, *GetUserPaymentsRequest) (*GetUserPaymentsResponse, error) { 149 | return nil, status.Errorf(codes.Unimplemented, "method GetUserPayments not implemented") 150 | } 151 | 152 | // UnsafePaymentServiceServer may be embedded to opt out of forward compatibility for this service. 153 | // Use of this interface is not recommended, as added methods to PaymentServiceServer will 154 | // result in compilation errors. 155 | type UnsafePaymentServiceServer interface { 156 | mustEmbedUnimplementedPaymentServiceServer() 157 | } 158 | 159 | func RegisterPaymentServiceServer(s grpc.ServiceRegistrar, srv PaymentServiceServer) { 160 | s.RegisterService(&PaymentService_ServiceDesc, srv) 161 | } 162 | 163 | func _PaymentService_CreatePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 164 | in := new(CreatePaymentRequest) 165 | if err := dec(in); err != nil { 166 | return nil, err 167 | } 168 | if interceptor == nil { 169 | return srv.(PaymentServiceServer).CreatePayment(ctx, in) 170 | } 171 | info := &grpc.UnaryServerInfo{ 172 | Server: srv, 173 | FullMethod: PaymentService_CreatePayment_FullMethodName, 174 | } 175 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 176 | return srv.(PaymentServiceServer).CreatePayment(ctx, req.(*CreatePaymentRequest)) 177 | } 178 | return interceptor(ctx, in, info, handler) 179 | } 180 | 181 | func _PaymentService_GetPayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 182 | in := new(GetPaymentRequest) 183 | if err := dec(in); err != nil { 184 | return nil, err 185 | } 186 | if interceptor == nil { 187 | return srv.(PaymentServiceServer).GetPayment(ctx, in) 188 | } 189 | info := &grpc.UnaryServerInfo{ 190 | Server: srv, 191 | FullMethod: PaymentService_GetPayment_FullMethodName, 192 | } 193 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 194 | return srv.(PaymentServiceServer).GetPayment(ctx, req.(*GetPaymentRequest)) 195 | } 196 | return interceptor(ctx, in, info, handler) 197 | } 198 | 199 | func _PaymentService_ListPayments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 200 | in := new(ListPaymentsRequest) 201 | if err := dec(in); err != nil { 202 | return nil, err 203 | } 204 | if interceptor == nil { 205 | return srv.(PaymentServiceServer).ListPayments(ctx, in) 206 | } 207 | info := &grpc.UnaryServerInfo{ 208 | Server: srv, 209 | FullMethod: PaymentService_ListPayments_FullMethodName, 210 | } 211 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 212 | return srv.(PaymentServiceServer).ListPayments(ctx, req.(*ListPaymentsRequest)) 213 | } 214 | return interceptor(ctx, in, info, handler) 215 | } 216 | 217 | func _PaymentService_UpdatePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 218 | in := new(UpdatePaymentRequest) 219 | if err := dec(in); err != nil { 220 | return nil, err 221 | } 222 | if interceptor == nil { 223 | return srv.(PaymentServiceServer).UpdatePayment(ctx, in) 224 | } 225 | info := &grpc.UnaryServerInfo{ 226 | Server: srv, 227 | FullMethod: PaymentService_UpdatePayment_FullMethodName, 228 | } 229 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 230 | return srv.(PaymentServiceServer).UpdatePayment(ctx, req.(*UpdatePaymentRequest)) 231 | } 232 | return interceptor(ctx, in, info, handler) 233 | } 234 | 235 | func _PaymentService_DeletePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 236 | in := new(DeletePaymentRequest) 237 | if err := dec(in); err != nil { 238 | return nil, err 239 | } 240 | if interceptor == nil { 241 | return srv.(PaymentServiceServer).DeletePayment(ctx, in) 242 | } 243 | info := &grpc.UnaryServerInfo{ 244 | Server: srv, 245 | FullMethod: PaymentService_DeletePayment_FullMethodName, 246 | } 247 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 248 | return srv.(PaymentServiceServer).DeletePayment(ctx, req.(*DeletePaymentRequest)) 249 | } 250 | return interceptor(ctx, in, info, handler) 251 | } 252 | 253 | func _PaymentService_GetUserPayments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 254 | in := new(GetUserPaymentsRequest) 255 | if err := dec(in); err != nil { 256 | return nil, err 257 | } 258 | if interceptor == nil { 259 | return srv.(PaymentServiceServer).GetUserPayments(ctx, in) 260 | } 261 | info := &grpc.UnaryServerInfo{ 262 | Server: srv, 263 | FullMethod: PaymentService_GetUserPayments_FullMethodName, 264 | } 265 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 266 | return srv.(PaymentServiceServer).GetUserPayments(ctx, req.(*GetUserPaymentsRequest)) 267 | } 268 | return interceptor(ctx, in, info, handler) 269 | } 270 | 271 | // PaymentService_ServiceDesc is the grpc.ServiceDesc for PaymentService service. 272 | // It's only intended for direct use with grpc.RegisterService, 273 | // and not to be introspected or modified (even as a copy) 274 | var PaymentService_ServiceDesc = grpc.ServiceDesc{ 275 | ServiceName: "payment.PaymentService", 276 | HandlerType: (*PaymentServiceServer)(nil), 277 | Methods: []grpc.MethodDesc{ 278 | { 279 | MethodName: "CreatePayment", 280 | Handler: _PaymentService_CreatePayment_Handler, 281 | }, 282 | { 283 | MethodName: "GetPayment", 284 | Handler: _PaymentService_GetPayment_Handler, 285 | }, 286 | { 287 | MethodName: "ListPayments", 288 | Handler: _PaymentService_ListPayments_Handler, 289 | }, 290 | { 291 | MethodName: "UpdatePayment", 292 | Handler: _PaymentService_UpdatePayment_Handler, 293 | }, 294 | { 295 | MethodName: "DeletePayment", 296 | Handler: _PaymentService_DeletePayment_Handler, 297 | }, 298 | { 299 | MethodName: "GetUserPayments", 300 | Handler: _PaymentService_GetUserPayments_Handler, 301 | }, 302 | }, 303 | Streams: []grpc.StreamDesc{}, 304 | Metadata: "api/proto/payment/payment.proto", 305 | } 306 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO - Vibe DDD Golang 2 | 3 | This document outlines planned improvements, features, and tasks for the Vibe DDD Golang project. Items are organized by priority and category following best practices from industry standards. 4 | 5 | ## 🎯 Quick Reference 6 | 7 | - 🔴 **Critical**: Security vulnerabilities, production blockers 8 | - 🟡 **High**: Important features, performance improvements 9 | - 🟢 **Medium**: Nice-to-have features, code quality improvements 10 | - 🔵 **Low**: Documentation, minor enhancements 11 | 12 | --- 13 | 14 | ## 🚀 Current Sprint (High Priority) 15 | 16 | ### 🔴 Critical Issues 17 | 18 | - [ ] **Fix Repository Count Query Performance** (Lines 63 in payment.repo.go) 19 | - Issue: `Count()` query runs before applying pagination filters 20 | - Impact: Performance degradation on large datasets 21 | - Solution: Optimize query by counting after filters, consider caching 22 | - Reference: [GORM Performance Best Practices](https://gorm.io/docs/performance.html) 23 | 24 | - [ ] **Add Database Transaction Support** 25 | - Issue: Repository operations lack transaction context 26 | - Impact: Data consistency risks 27 | - Solution: Implement `WithTx(tx *gorm.DB)` pattern 28 | - Reference: [Database Transaction Patterns](https://github.com/uber-go/guide/blob/master/style.md#database-transactions) 29 | 30 | - [ ] **Implement Proper Error Handling** 31 | - Issue: Generic error responses expose internal details 32 | - Impact: Security and user experience 33 | - Solution: Add domain-specific error types 34 | - Reference: [Go Error Handling Best Practices](https://github.com/uber-go/guide/blob/master/style.md#errors) 35 | 36 | ### 🟡 High Priority Features 37 | 38 | - [ ] **Add Input Validation Middleware** 39 | - Implement comprehensive request validation 40 | - Add custom validation rules for business logic 41 | - Reference: [Gin Validation Guide](https://gin-gonic.com/docs/examples/binding-and-validation/) 42 | 43 | - [ ] **Implement API Rate Limiting** 44 | - Add Redis-based rate limiting 45 | - Configure per-endpoint limits 46 | - Reference: [Rate Limiting Patterns](https://cloud.google.com/architecture/rate-limiting-strategies-techniques) 47 | 48 | - [ ] **Add Structured Logging Context** 49 | - Implement request ID tracing 50 | - Add correlation IDs across services 51 | - Reference: [Go Logging Best Practices](https://github.com/uber-go/zap/blob/master/README.md#performance) 52 | 53 | --- 54 | 55 | ## 🏗️ Architecture Improvements 56 | 57 | ### 🟢 Domain-Driven Design Enhancements 58 | 59 | - [ ] **Implement Domain Events** 60 | - Add event sourcing for payment state changes 61 | - Implement event handlers for cross-domain communication 62 | - Reference: [Domain Events in Go](https://www.oreilly.com/library/view/domain-driven-design/9780321125215/) 63 | 64 | - [ ] **Add Command Query Responsibility Segregation (CQRS)** 65 | - Separate read and write models 66 | - Optimize query performance 67 | - Reference: [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) 68 | 69 | - [ ] **Implement Aggregate Root Pattern** 70 | - Enforce business rules at aggregate boundaries 71 | - Add proper aggregate validation 72 | - Reference: [Aggregate Design](https://dddcommunity.org/library/vernon_2011/) 73 | 74 | ### 🟢 Microservice Patterns 75 | 76 | - [ ] **Add Circuit Breaker Pattern** 77 | - Implement for external service calls 78 | - Add fallback mechanisms 79 | - Reference: [Circuit Breaker Pattern](https://martinfowler.com/bliki/CircuitBreaker.html) 80 | 81 | - [ ] **Implement Saga Pattern** 82 | - Handle distributed transactions 83 | - Add compensation logic 84 | - Reference: [Saga Pattern](https://microservices.io/patterns/data/saga.html) 85 | 86 | - [ ] **Add Service Mesh Support** 87 | - Implement Istio/Linkerd compatibility 88 | - Add observability features 89 | - Reference: [Service Mesh Patterns](https://www.oreilly.com/library/view/istio-up-and/9781492043775/) 90 | 91 | --- 92 | 93 | ## 🔒 Security Enhancements 94 | 95 | ### 🔴 Authentication & Authorization 96 | 97 | - [ ] **Implement JWT Authentication** 98 | - Add JWT token generation and validation 99 | - Implement refresh token mechanism 100 | - Reference: [JWT Best Practices](https://tools.ietf.org/html/rfc7519) 101 | 102 | - [ ] **Add Role-Based Access Control (RBAC)** 103 | - Define user roles and permissions 104 | - Implement middleware for authorization 105 | - Reference: [RBAC in Go](https://github.com/casbin/casbin) 106 | 107 | - [ ] **Implement API Key Management** 108 | - Add API key generation and validation 109 | - Implement key rotation 110 | - Reference: [API Security Best Practices](https://owasp.org/www-project-api-security/) 111 | 112 | ### 🟡 Data Protection 113 | 114 | - [ ] **Add Field-Level Encryption** 115 | - Encrypt sensitive data at rest 116 | - Implement key management 117 | - Reference: [Go Encryption Best Practices](https://golang.org/pkg/crypto/) 118 | 119 | - [ ] **Implement Input Sanitization** 120 | - Add SQL injection protection 121 | - Implement XSS prevention 122 | - Reference: [OWASP Go Security](https://owasp.org/www-project-go-secure-coding-practices-guide/) 123 | 124 | - [ ] **Add Audit Logging** 125 | - Track all data modifications 126 | - Implement compliance reporting 127 | - Reference: [Audit Logging Standards](https://www.sans.org/white-papers/1168/) 128 | 129 | --- 130 | 131 | ## ⚡ Performance Optimizations 132 | 133 | ### 🟡 Database Performance 134 | 135 | - [ ] **Implement Database Connection Pooling** 136 | - Optimize connection pool settings 137 | - Add connection health checks 138 | - Reference: [Go Database Best Practices](https://github.com/go-sql-driver/mysql#connection-pool-and-timeouts) 139 | 140 | - [ ] **Add Database Indexing Strategy** 141 | - Analyze query patterns 142 | - Implement composite indexes 143 | - Reference: [PostgreSQL Performance Tuning](https://wiki.postgresql.org/wiki/Performance_Optimization) 144 | 145 | - [ ] **Implement Query Optimization** 146 | - Add query result caching 147 | - Implement pagination best practices 148 | - Reference: [GORM Performance Guide](https://gorm.io/docs/performance.html) 149 | 150 | ### 🟢 Caching Strategy 151 | 152 | - [ ] **Implement Redis Caching** 153 | - Add application-level caching 154 | - Implement cache invalidation strategies 155 | - Reference: [Redis Best Practices](https://redis.io/docs/manual/clients-guide/) 156 | 157 | - [ ] **Add CDN Integration** 158 | - Implement static asset caching 159 | - Add edge caching for API responses 160 | - Reference: [CDN Best Practices](https://developers.cloudflare.com/cache/about/cache-performance/) 161 | 162 | ### 🟢 Monitoring & Observability 163 | 164 | - [ ] **Implement Distributed Tracing** 165 | - Add OpenTelemetry integration 166 | - Implement trace correlation 167 | - Reference: [OpenTelemetry Go](https://opentelemetry.io/docs/instrumentation/go/) 168 | 169 | - [ ] **Add Metrics Collection** 170 | - Implement Prometheus metrics 171 | - Add custom business metrics 172 | - Reference: [Go Metrics Best Practices](https://prometheus.io/docs/guides/go-application/) 173 | 174 | - [ ] **Implement Health Checks** 175 | - Add comprehensive health endpoints 176 | - Implement dependency health checks 177 | - Reference: [Health Check Patterns](https://microservices.io/patterns/observability/health-check-api.html) 178 | 179 | --- 180 | 181 | ## 🧪 Testing Improvements 182 | 183 | ### 🟡 Test Coverage 184 | 185 | - [ ] **Increase Unit Test Coverage to 90%+** 186 | - Add missing test cases 187 | - Implement table-driven tests 188 | - Reference: [Go Testing Best Practices](https://github.com/golang/go/wiki/TestComments) 189 | 190 | - [ ] **Add Integration Tests** 191 | - Test database interactions 192 | - Test API endpoints end-to-end 193 | - Reference: [Integration Testing in Go](https://peter.bourgon.org/go-in-production/#testing-and-validation) 194 | 195 | - [ ] **Implement Contract Testing** 196 | - Add Pact testing for API contracts 197 | - Test gRPC service contracts 198 | - Reference: [Contract Testing Guide](https://pact.io/) 199 | 200 | ### 🟢 Test Infrastructure 201 | 202 | - [ ] **Add Testcontainers Support** 203 | - Implement database testing with real databases 204 | - Add Redis testing containers 205 | - Reference: [Testcontainers Go](https://golang.testcontainers.org/) 206 | 207 | - [ ] **Implement Benchmark Tests** 208 | - Add performance benchmarks 209 | - Implement load testing 210 | - Reference: [Go Benchmarking](https://golang.org/pkg/testing/#hdr-Benchmarks) 211 | 212 | - [ ] **Add Mutation Testing** 213 | - Verify test quality 214 | - Implement mutation testing pipeline 215 | - Reference: [Mutation Testing in Go](https://github.com/go-mutesting/mutesting) 216 | 217 | --- 218 | 219 | ## 🔧 DevOps & Infrastructure 220 | 221 | ### 🟡 CI/CD Pipeline 222 | 223 | - [ ] **Implement GitOps Workflow** 224 | - Add ArgoCD or Flux integration 225 | - Implement automated deployments 226 | - Reference: [GitOps Best Practices](https://www.gitops.tech/) 227 | 228 | - [ ] **Add Container Security Scanning** 229 | - Implement vulnerability scanning 230 | - Add dependency checking 231 | - Reference: [Container Security](https://sysdig.com/blog/container-security-best-practices/) 232 | 233 | - [ ] **Implement Multi-Environment Strategy** 234 | - Add staging and production environments 235 | - Implement environment-specific configurations 236 | - Reference: [Environment Management](https://12factor.net/) 237 | 238 | ### 🟢 Kubernetes Integration 239 | 240 | - [ ] **Add Kubernetes Manifests** 241 | - Implement Helm charts 242 | - Add resource management 243 | - Reference: [Kubernetes Best Practices](https://kubernetes.io/docs/concepts/configuration/overview/) 244 | 245 | - [ ] **Implement Horizontal Pod Autoscaling** 246 | - Add metrics-based scaling 247 | - Implement custom metrics 248 | - Reference: [HPA Guide](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) 249 | 250 | --- 251 | 252 | ## 📚 Documentation & Developer Experience 253 | 254 | ### 🟢 Documentation 255 | 256 | - [ ] **Add API Documentation Examples** 257 | - Implement comprehensive Postman collections 258 | - Add curl examples for all endpoints 259 | - Reference: [API Documentation Best Practices](https://swagger.io/resources/articles/documenting-apis/) 260 | 261 | - [ ] **Create Developer Onboarding Guide** 262 | - Add step-by-step setup instructions 263 | - Implement local development environment 264 | - Reference: [Developer Experience Best Practices](https://dx.tips/) 265 | 266 | - [ ] **Add Architecture Decision Records (ADRs)** 267 | - Document architectural decisions 268 | - Implement ADR template 269 | - Reference: [ADR Best Practices](https://github.com/joelparkerhenderson/architecture-decision-record) 270 | 271 | ### 🔵 Code Quality 272 | 273 | - [ ] **Implement Code Review Guidelines** 274 | - Add pull request templates 275 | - Implement review checklists 276 | - Reference: [Code Review Best Practices](https://google.github.io/eng-practices/review/) 277 | 278 | - [ ] **Add Code Generation Tools** 279 | - Implement mock generation 280 | - Add code scaffolding tools 281 | - Reference: [Go Code Generation](https://blog.golang.org/generate) 282 | 283 | --- 284 | 285 | ## 🎯 Feature Roadmap 286 | 287 | ### 🟢 V1.1 Release 288 | 289 | - [ ] **User Profile Management** 290 | - Add user avatar upload 291 | - Implement profile preferences 292 | - Add user activity logging 293 | 294 | - [ ] **Payment Gateway Integration** 295 | - Add Stripe integration 296 | - Implement PayPal support 297 | - Add cryptocurrency payments 298 | 299 | - [ ] **Notification System** 300 | - Add email notifications 301 | - Implement push notifications 302 | - Add SMS integration 303 | 304 | ### 🔵 V1.2 Release 305 | 306 | - [ ] **Analytics Dashboard** 307 | - Add user analytics 308 | - Implement payment analytics 309 | - Add business intelligence features 310 | 311 | - [ ] **Multi-Tenancy Support** 312 | - Add tenant isolation 313 | - Implement tenant-specific configurations 314 | - Add tenant management APIs 315 | 316 | --- 317 | 318 | ## 📖 References & Best Practices 319 | 320 | ### 📘 Architecture & Design 321 | 322 | - [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - Uncle Bob Martin 323 | - [Domain-Driven Design](https://martinfowler.com/tags/domain%20driven%20design.html) - Martin Fowler 324 | - [Microservices Patterns](https://microservices.io/patterns/index.html) - Chris Richardson 325 | - [Go Project Layout](https://github.com/golang-standards/project-layout) - Standard Go Project Structure 326 | 327 | ### 📗 Security 328 | 329 | - [OWASP API Security](https://owasp.org/www-project-api-security/) - API Security Best Practices 330 | - [Go Security](https://github.com/securego/gosec) - Security Analyzer for Go 331 | - [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework) - Security Standards 332 | 333 | ### 📙 Performance & Scalability 334 | 335 | - [High Performance Go](https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html) - Dave Cheney 336 | - [Go Performance Tuning](https://github.com/dgryski/go-perfbook) - Performance Book 337 | - [Database Performance](https://use-the-index-luke.com/) - SQL Performance Guide 338 | 339 | ### 📕 Testing 340 | 341 | - [Go Testing](https://github.com/golang/go/wiki/TestComments) - Official Go Testing Guide 342 | - [Testing Strategies](https://martinfowler.com/articles/practical-test-pyramid.html) - Test Pyramid 343 | - [Behavior-Driven Development](https://cucumber.io/docs/bdd/) - BDD Best Practices 344 | 345 | ### 📒 DevOps 346 | 347 | - [12-Factor App](https://12factor.net/) - Methodology for SaaS Apps 348 | - [Kubernetes Patterns](https://k8spatterns.io/) - Kubernetes Design Patterns 349 | - [Site Reliability Engineering](https://sre.google/books/) - Google SRE Book 350 | 351 | --- 352 | 353 | ## 📋 Contributing Guidelines 354 | 355 | When working on TODO items: 356 | 357 | 1. **Create Feature Branch**: Use descriptive branch names (e.g., `feature/jwt-authentication`) 358 | 2. **Update Documentation**: Update relevant docs when implementing features 359 | 3. **Add Tests**: Ensure adequate test coverage for new features 360 | 4. **Follow Conventions**: Adhere to existing code style and patterns 361 | 5. **Update TODO**: Mark items as complete and add new items as needed 362 | 363 | ### Priority Guidelines 364 | 365 | - **Critical**: Address immediately, block releases 366 | - **High**: Include in current sprint 367 | - **Medium**: Plan for next iteration 368 | - **Low**: Address when capacity allows 369 | 370 | --- 371 | 372 | *Last Updated: July 2025* 373 | *Maintainers: Development Team* -------------------------------------------------------------------------------- /internal/application/user/handler/user.handler_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | "vibe-ddd-golang/internal/application/user/dto" 13 | "vibe-ddd-golang/internal/pkg/testutil" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/mock" 18 | ) 19 | 20 | func setupUserHandler() (*UserHandler, *testutil.MockUserService) { 21 | gin.SetMode(gin.TestMode) 22 | mockService := &testutil.MockUserService{} 23 | logger := testutil.NewSilentLogger() 24 | handler := NewUserHandler(mockService, logger) 25 | return handler, mockService 26 | } 27 | 28 | func TestUserHandler_CreateUser(t *testing.T) { 29 | t.Run("should create user successfully", func(t *testing.T) { 30 | // Setup 31 | handler, mockService := setupUserHandler() 32 | 33 | req := testutil.CreateUserRequestFixture() 34 | response := &dto.UserResponse{ 35 | ID: 1, 36 | Name: req.Name, 37 | Email: req.Email, 38 | CreatedAt: time.Now(), 39 | UpdatedAt: time.Now(), 40 | } 41 | 42 | mockService.On("CreateUser", mock.AnythingOfType("*dto.CreateUserRequest")).Return(response, nil) 43 | 44 | // Prepare request 45 | reqBody, _ := json.Marshal(req) 46 | w := httptest.NewRecorder() 47 | ctx, _ := gin.CreateTestContext(w) 48 | ctx.Request = httptest.NewRequest("POST", "/users", bytes.NewBuffer(reqBody)) 49 | ctx.Request.Header.Set("Content-Type", "application/json") 50 | 51 | // When 52 | handler.CreateUser(ctx) 53 | 54 | // Then 55 | assert.Equal(t, http.StatusCreated, w.Code) 56 | mockService.AssertExpectations(t) 57 | 58 | var result map[string]interface{} 59 | json.Unmarshal(w.Body.Bytes(), &result) 60 | assert.Contains(t, result, "data") 61 | data := result["data"].(map[string]interface{}) 62 | assert.Equal(t, float64(1), data["id"]) 63 | assert.Equal(t, req.Name, data["name"]) 64 | assert.Equal(t, req.Email, data["email"]) 65 | }) 66 | 67 | t.Run("should return bad request for invalid JSON", func(t *testing.T) { 68 | // Setup 69 | handler, mockService := setupUserHandler() 70 | 71 | w := httptest.NewRecorder() 72 | ctx, _ := gin.CreateTestContext(w) 73 | ctx.Request = httptest.NewRequest("POST", "/users", bytes.NewBuffer([]byte("invalid json"))) 74 | ctx.Request.Header.Set("Content-Type", "application/json") 75 | 76 | // When 77 | handler.CreateUser(ctx) 78 | 79 | // Then 80 | assert.Equal(t, http.StatusBadRequest, w.Code) 81 | mockService.AssertExpectations(t) 82 | }) 83 | 84 | t.Run("should return conflict when email already exists", func(t *testing.T) { 85 | // Setup 86 | handler, mockService := setupUserHandler() 87 | 88 | req := testutil.CreateUserRequestFixture() 89 | mockService.On("CreateUser", mock.AnythingOfType("*dto.CreateUserRequest")).Return(nil, errors.New("email already exists")) 90 | 91 | reqBody, _ := json.Marshal(req) 92 | w := httptest.NewRecorder() 93 | ctx, _ := gin.CreateTestContext(w) 94 | ctx.Request = httptest.NewRequest("POST", "/users", bytes.NewBuffer(reqBody)) 95 | ctx.Request.Header.Set("Content-Type", "application/json") 96 | 97 | // When 98 | handler.CreateUser(ctx) 99 | 100 | // Then 101 | assert.Equal(t, http.StatusConflict, w.Code) 102 | mockService.AssertExpectations(t) 103 | }) 104 | 105 | t.Run("should return internal api error for other errors", func(t *testing.T) { 106 | // Setup 107 | handler, mockService := setupUserHandler() 108 | 109 | req := testutil.CreateUserRequestFixture() 110 | mockService.On("CreateUser", mock.AnythingOfType("*dto.CreateUserRequest")).Return(nil, errors.New("database error")) 111 | 112 | reqBody, _ := json.Marshal(req) 113 | w := httptest.NewRecorder() 114 | ctx, _ := gin.CreateTestContext(w) 115 | ctx.Request = httptest.NewRequest("POST", "/users", bytes.NewBuffer(reqBody)) 116 | ctx.Request.Header.Set("Content-Type", "application/json") 117 | 118 | // When 119 | handler.CreateUser(ctx) 120 | 121 | // Then 122 | assert.Equal(t, http.StatusInternalServerError, w.Code) 123 | mockService.AssertExpectations(t) 124 | }) 125 | } 126 | 127 | func TestUserHandler_GetUser(t *testing.T) { 128 | t.Run("should get user successfully", func(t *testing.T) { 129 | // Setup 130 | handler, mockService := setupUserHandler() 131 | 132 | userID := uint(1) 133 | response := &dto.UserResponse{ 134 | ID: userID, 135 | Name: "John Doe", 136 | Email: "john@example.com", 137 | CreatedAt: time.Now(), 138 | UpdatedAt: time.Now(), 139 | } 140 | 141 | mockService.On("GetUserByID", userID).Return(response, nil) 142 | 143 | w := httptest.NewRecorder() 144 | ctx, _ := gin.CreateTestContext(w) 145 | ctx.Request = httptest.NewRequest("GET", "/users/1", nil) 146 | ctx.Params = gin.Params{ 147 | {Key: "id", Value: "1"}, 148 | } 149 | 150 | // When 151 | handler.GetUser(ctx) 152 | 153 | // Then 154 | assert.Equal(t, http.StatusOK, w.Code) 155 | mockService.AssertExpectations(t) 156 | 157 | var result map[string]interface{} 158 | json.Unmarshal(w.Body.Bytes(), &result) 159 | assert.Contains(t, result, "data") 160 | data := result["data"].(map[string]interface{}) 161 | assert.Equal(t, float64(1), data["id"]) 162 | }) 163 | 164 | t.Run("should return bad request for invalid ID", func(t *testing.T) { 165 | // Setup 166 | handler, mockService := setupUserHandler() 167 | 168 | w := httptest.NewRecorder() 169 | ctx, _ := gin.CreateTestContext(w) 170 | ctx.Request = httptest.NewRequest("GET", "/users/invalid", nil) 171 | ctx.Params = gin.Params{ 172 | {Key: "id", Value: "invalid"}, 173 | } 174 | 175 | // When 176 | handler.GetUser(ctx) 177 | 178 | // Then 179 | assert.Equal(t, http.StatusBadRequest, w.Code) 180 | mockService.AssertExpectations(t) 181 | }) 182 | 183 | t.Run("should return not found when user not found", func(t *testing.T) { 184 | // Setup 185 | handler, mockService := setupUserHandler() 186 | 187 | userID := uint(999) 188 | mockService.On("GetUserByID", userID).Return(nil, errors.New("user not found")) 189 | 190 | w := httptest.NewRecorder() 191 | ctx, _ := gin.CreateTestContext(w) 192 | ctx.Request = httptest.NewRequest("GET", "/users/999", nil) 193 | ctx.Params = gin.Params{ 194 | {Key: "id", Value: "999"}, 195 | } 196 | 197 | // When 198 | handler.GetUser(ctx) 199 | 200 | // Then 201 | assert.Equal(t, http.StatusNotFound, w.Code) 202 | mockService.AssertExpectations(t) 203 | }) 204 | } 205 | 206 | func TestUserHandler_GetUsers(t *testing.T) { 207 | t.Run("should get users successfully", func(t *testing.T) { 208 | // Setup 209 | handler, mockService := setupUserHandler() 210 | 211 | response := &dto.UserListResponse{ 212 | Data: []dto.UserResponse{ 213 | {ID: 1, Name: "User 1", Email: "user1@example.com"}, 214 | {ID: 2, Name: "User 2", Email: "user2@example.com"}, 215 | }, 216 | TotalCount: 2, 217 | Page: 1, 218 | PageSize: 10, 219 | } 220 | 221 | mockService.On("GetUsers", mock.AnythingOfType("*dto.UserFilter")).Return(response, nil) 222 | 223 | w := httptest.NewRecorder() 224 | ctx, _ := gin.CreateTestContext(w) 225 | ctx.Request = httptest.NewRequest("GET", "/users?page=1&page_size=10", nil) 226 | 227 | // When 228 | handler.GetUsers(ctx) 229 | 230 | // Then 231 | assert.Equal(t, http.StatusOK, w.Code) 232 | mockService.AssertExpectations(t) 233 | 234 | var result dto.UserListResponse 235 | json.Unmarshal(w.Body.Bytes(), &result) 236 | assert.Len(t, result.Data, 2) 237 | assert.Equal(t, int64(2), result.TotalCount) 238 | }) 239 | 240 | t.Run("should return bad request for invalid query parameters", func(t *testing.T) { 241 | // Setup 242 | handler, mockService := setupUserHandler() 243 | 244 | w := httptest.NewRecorder() 245 | ctx, _ := gin.CreateTestContext(w) 246 | ctx.Request = httptest.NewRequest("GET", "/users?page=invalid", nil) 247 | 248 | // When 249 | handler.GetUsers(ctx) 250 | 251 | // Then 252 | assert.Equal(t, http.StatusBadRequest, w.Code) 253 | mockService.AssertExpectations(t) 254 | }) 255 | 256 | t.Run("should return internal api error when service fails", func(t *testing.T) { 257 | // Setup 258 | handler, mockService := setupUserHandler() 259 | 260 | mockService.On("GetUsers", mock.AnythingOfType("*dto.UserFilter")).Return(nil, errors.New("database error")) 261 | 262 | w := httptest.NewRecorder() 263 | ctx, _ := gin.CreateTestContext(w) 264 | ctx.Request = httptest.NewRequest("GET", "/users", nil) 265 | 266 | // When 267 | handler.GetUsers(ctx) 268 | 269 | // Then 270 | assert.Equal(t, http.StatusInternalServerError, w.Code) 271 | mockService.AssertExpectations(t) 272 | }) 273 | } 274 | 275 | func TestUserHandler_UpdateUser(t *testing.T) { 276 | t.Run("should update user successfully", func(t *testing.T) { 277 | // Setup 278 | handler, mockService := setupUserHandler() 279 | 280 | userID := uint(1) 281 | req := testutil.CreateUpdateUserRequestFixture() 282 | response := &dto.UserResponse{ 283 | ID: userID, 284 | Name: req.Name, 285 | Email: req.Email, 286 | CreatedAt: time.Now(), 287 | UpdatedAt: time.Now(), 288 | } 289 | 290 | mockService.On("UpdateUser", userID, mock.AnythingOfType("*dto.UpdateUserRequest")).Return(response, nil) 291 | 292 | reqBody, _ := json.Marshal(req) 293 | w := httptest.NewRecorder() 294 | ctx, _ := gin.CreateTestContext(w) 295 | ctx.Request = httptest.NewRequest("PUT", "/users/1", bytes.NewBuffer(reqBody)) 296 | ctx.Request.Header.Set("Content-Type", "application/json") 297 | ctx.Params = gin.Params{ 298 | {Key: "id", Value: "1"}, 299 | } 300 | 301 | // When 302 | handler.UpdateUser(ctx) 303 | 304 | // Then 305 | assert.Equal(t, http.StatusOK, w.Code) 306 | mockService.AssertExpectations(t) 307 | 308 | var result map[string]interface{} 309 | json.Unmarshal(w.Body.Bytes(), &result) 310 | assert.Contains(t, result, "data") 311 | data := result["data"].(map[string]interface{}) 312 | assert.Equal(t, float64(1), data["id"]) 313 | }) 314 | 315 | t.Run("should return bad request for invalid ID", func(t *testing.T) { 316 | // Setup 317 | handler, mockService := setupUserHandler() 318 | 319 | req := testutil.CreateUpdateUserRequestFixture() 320 | reqBody, _ := json.Marshal(req) 321 | w := httptest.NewRecorder() 322 | ctx, _ := gin.CreateTestContext(w) 323 | ctx.Request = httptest.NewRequest("PUT", "/users/invalid", bytes.NewBuffer(reqBody)) 324 | ctx.Request.Header.Set("Content-Type", "application/json") 325 | ctx.Params = gin.Params{ 326 | {Key: "id", Value: "invalid"}, 327 | } 328 | 329 | // When 330 | handler.UpdateUser(ctx) 331 | 332 | // Then 333 | assert.Equal(t, http.StatusBadRequest, w.Code) 334 | mockService.AssertExpectations(t) 335 | }) 336 | 337 | t.Run("should return not found when user not found", func(t *testing.T) { 338 | // Setup 339 | handler, mockService := setupUserHandler() 340 | 341 | userID := uint(999) 342 | req := testutil.CreateUpdateUserRequestFixture() 343 | mockService.On("UpdateUser", userID, mock.AnythingOfType("*dto.UpdateUserRequest")).Return(nil, errors.New("user not found")) 344 | 345 | reqBody, _ := json.Marshal(req) 346 | w := httptest.NewRecorder() 347 | ctx, _ := gin.CreateTestContext(w) 348 | ctx.Request = httptest.NewRequest("PUT", "/users/999", bytes.NewBuffer(reqBody)) 349 | ctx.Request.Header.Set("Content-Type", "application/json") 350 | ctx.Params = gin.Params{ 351 | {Key: "id", Value: "999"}, 352 | } 353 | 354 | // When 355 | handler.UpdateUser(ctx) 356 | 357 | // Then 358 | assert.Equal(t, http.StatusNotFound, w.Code) 359 | mockService.AssertExpectations(t) 360 | }) 361 | 362 | t.Run("should return conflict when email already exists", func(t *testing.T) { 363 | // Setup 364 | handler, mockService := setupUserHandler() 365 | 366 | userID := uint(1) 367 | req := testutil.CreateUpdateUserRequestFixture() 368 | mockService.On("UpdateUser", userID, mock.AnythingOfType("*dto.UpdateUserRequest")).Return(nil, errors.New("email already exists")) 369 | 370 | reqBody, _ := json.Marshal(req) 371 | w := httptest.NewRecorder() 372 | ctx, _ := gin.CreateTestContext(w) 373 | ctx.Request = httptest.NewRequest("PUT", "/users/1", bytes.NewBuffer(reqBody)) 374 | ctx.Request.Header.Set("Content-Type", "application/json") 375 | ctx.Params = gin.Params{ 376 | {Key: "id", Value: "1"}, 377 | } 378 | 379 | // When 380 | handler.UpdateUser(ctx) 381 | 382 | // Then 383 | assert.Equal(t, http.StatusConflict, w.Code) 384 | mockService.AssertExpectations(t) 385 | }) 386 | } 387 | 388 | func TestUserHandler_UpdateUserPassword(t *testing.T) { 389 | t.Run("should update user password successfully", func(t *testing.T) { 390 | // Setup 391 | handler, mockService := setupUserHandler() 392 | 393 | userID := uint(1) 394 | req := &dto.UpdateUserPasswordRequest{ 395 | CurrentPassword: "oldpassword", 396 | NewPassword: "newpassword123", 397 | } 398 | 399 | mockService.On("UpdateUserPassword", userID, mock.AnythingOfType("*dto.UpdateUserPasswordRequest")).Return(nil) 400 | 401 | reqBody, _ := json.Marshal(req) 402 | w := httptest.NewRecorder() 403 | ctx, _ := gin.CreateTestContext(w) 404 | ctx.Request = httptest.NewRequest("PUT", "/users/1/password", bytes.NewBuffer(reqBody)) 405 | ctx.Request.Header.Set("Content-Type", "application/json") 406 | ctx.Params = gin.Params{ 407 | {Key: "id", Value: "1"}, 408 | } 409 | 410 | // When 411 | handler.UpdateUserPassword(ctx) 412 | 413 | // Then 414 | assert.Equal(t, http.StatusOK, w.Code) 415 | mockService.AssertExpectations(t) 416 | 417 | var result map[string]interface{} 418 | json.Unmarshal(w.Body.Bytes(), &result) 419 | assert.Contains(t, result, "message") 420 | assert.Equal(t, "Password updated successfully", result["message"]) 421 | }) 422 | 423 | t.Run("should return unauthorized when current password is incorrect", func(t *testing.T) { 424 | // Setup 425 | handler, mockService := setupUserHandler() 426 | 427 | userID := uint(1) 428 | req := &dto.UpdateUserPasswordRequest{ 429 | CurrentPassword: "wrongpassword", 430 | NewPassword: "newpassword123", 431 | } 432 | 433 | mockService.On("UpdateUserPassword", userID, mock.AnythingOfType("*dto.UpdateUserPasswordRequest")).Return(errors.New("current password is incorrect")) 434 | 435 | reqBody, _ := json.Marshal(req) 436 | w := httptest.NewRecorder() 437 | ctx, _ := gin.CreateTestContext(w) 438 | ctx.Request = httptest.NewRequest("PUT", "/users/1/password", bytes.NewBuffer(reqBody)) 439 | ctx.Request.Header.Set("Content-Type", "application/json") 440 | ctx.Params = gin.Params{ 441 | {Key: "id", Value: "1"}, 442 | } 443 | 444 | // When 445 | handler.UpdateUserPassword(ctx) 446 | 447 | // Then 448 | assert.Equal(t, http.StatusUnauthorized, w.Code) 449 | mockService.AssertExpectations(t) 450 | }) 451 | } 452 | 453 | func TestUserHandler_DeleteUser(t *testing.T) { 454 | t.Run("should delete user successfully", func(t *testing.T) { 455 | // Setup 456 | handler, mockService := setupUserHandler() 457 | 458 | userID := uint(1) 459 | mockService.On("DeleteUser", userID).Return(nil) 460 | 461 | w := httptest.NewRecorder() 462 | ctx, _ := gin.CreateTestContext(w) 463 | ctx.Request = httptest.NewRequest("DELETE", "/users/1", nil) 464 | ctx.Params = gin.Params{ 465 | {Key: "id", Value: "1"}, 466 | } 467 | 468 | // When 469 | handler.DeleteUser(ctx) 470 | 471 | // Then 472 | assert.Equal(t, http.StatusOK, w.Code) 473 | mockService.AssertExpectations(t) 474 | 475 | var result map[string]interface{} 476 | json.Unmarshal(w.Body.Bytes(), &result) 477 | assert.Contains(t, result, "message") 478 | assert.Equal(t, "User deleted successfully", result["message"]) 479 | }) 480 | 481 | t.Run("should return not found when user not found", func(t *testing.T) { 482 | // Setup 483 | handler, mockService := setupUserHandler() 484 | 485 | userID := uint(999) 486 | mockService.On("DeleteUser", userID).Return(errors.New("user not found")) 487 | 488 | w := httptest.NewRecorder() 489 | ctx, _ := gin.CreateTestContext(w) 490 | ctx.Request = httptest.NewRequest("DELETE", "/users/999", nil) 491 | ctx.Params = gin.Params{ 492 | {Key: "id", Value: "999"}, 493 | } 494 | 495 | // When 496 | handler.DeleteUser(ctx) 497 | 498 | // Then 499 | assert.Equal(t, http.StatusNotFound, w.Code) 500 | mockService.AssertExpectations(t) 501 | }) 502 | 503 | t.Run("should return bad request for invalid ID", func(t *testing.T) { 504 | // Setup 505 | handler, mockService := setupUserHandler() 506 | 507 | w := httptest.NewRecorder() 508 | ctx, _ := gin.CreateTestContext(w) 509 | ctx.Request = httptest.NewRequest("DELETE", "/users/invalid", nil) 510 | ctx.Params = gin.Params{ 511 | {Key: "id", Value: "invalid"}, 512 | } 513 | 514 | // When 515 | handler.DeleteUser(ctx) 516 | 517 | // Then 518 | assert.Equal(t, http.StatusBadRequest, w.Code) 519 | mockService.AssertExpectations(t) 520 | }) 521 | } 522 | 523 | func TestUserHandler_RegisterRoutes(t *testing.T) { 524 | t.Run("should register all routes correctly", func(t *testing.T) { 525 | // Setup 526 | handler, _ := setupUserHandler() 527 | router := gin.New() 528 | api := router.Group("/api/v1") 529 | 530 | // When 531 | handler.RegisterRoutes(api) 532 | 533 | // Then 534 | routes := router.Routes() 535 | expectedRoutes := []string{ 536 | "POST /api/v1/users", 537 | "GET /api/v1/users", 538 | "GET /api/v1/users/:id", 539 | "PUT /api/v1/users/:id", 540 | "DELETE /api/v1/users/:id", 541 | "PUT /api/v1/users/:id/password", 542 | } 543 | 544 | assert.Len(t, routes, len(expectedRoutes)) 545 | for _, expectedRoute := range expectedRoutes { 546 | found := false 547 | for _, route := range routes { 548 | if route.Method+" "+route.Path == expectedRoute { 549 | found = true 550 | break 551 | } 552 | } 553 | assert.True(t, found, "Route %s not found", expectedRoute) 554 | } 555 | }) 556 | } 557 | --------------------------------------------------------------------------------