diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..347e267 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +volumes/ \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2581b7d --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Postgres configuration +POSTGRES_HOST=gss-postgres-db +POSTGRES_PORT=5432 +POSTGRES_USER=root +POSTGRES_PASSWORD=root +POSTGRES_DB=gss-db + +# Fiber configuration +FIBER_PORT=3000 + +# SMTP configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_EMAIL= +SMTP_PASSWORD= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cde8a1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +.env.production +volumes/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..77f4998 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Setting up my base image +FROM golang:1.24.1-alpine + +# Set the Current Working Directory inside the container +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Get all dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the Go app +RUN go build -o gss-backend ./cmd + +# Expose port 3001 +EXPOSE 3001 + +# Run the application +CMD ["./gss-backend"] \ No newline at end of file diff --git a/api/dtos/user_dto.go b/api/dtos/user_dto.go new file mode 100644 index 0000000..a805f60 --- /dev/null +++ b/api/dtos/user_dto.go @@ -0,0 +1,8 @@ +package dtos + +type CreateUserDTO struct { + FullName string `json:"full_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + ReferrerCode string `json:"referrer_code"` +} \ No newline at end of file diff --git a/api/handlers/user.go b/api/handlers/user.go new file mode 100644 index 0000000..05548db --- /dev/null +++ b/api/handlers/user.go @@ -0,0 +1,76 @@ +package handlers + +import ( + "errors" + "gss-backend/api/dtos" + "gss-backend/api/presenters" + services "gss-backend/pkg/services/user" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +func CreateUser(user_service services.IUserService) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody dtos.CreateUserDTO + + err := c.BodyParser(&requestBody) + + if err != nil { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenters.UserErrorResponse(err)) + } + + if requestBody.FullName == "" || requestBody.Email == "" || requestBody.PhoneNumber == "" { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenters.UserErrorResponse(errors.New( + "Full Name, Email, and Phone Number are required", + ))) + } + + result, err := user_service.Create(&requestBody) + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenters.UserErrorResponse(err)) + } + + return c.JSON(presenters.UserSuccessResponse(result)) + + } +} + +func FindAllUsers(user_service services.IUserService) fiber.Handler { + return func(c *fiber.Ctx) error { + result, err := user_service.FindAll() + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenters.UserErrorResponse(err)) + } + + return c.JSON(presenters.UsersSuccessResponse(result)) + } +} + +func FindUserByID(user_service services.IUserService) fiber.Handler { + return func(c *fiber.Ctx) error { + idStr := c.Params("id") + + id, err := strconv.ParseUint(idStr, 10, 32) + + if err != nil { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenters.UserErrorResponse(err)) + } + + result, err := user_service.FindByID(uint(id)) + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenters.UserErrorResponse(err)) + } + + return c.JSON(presenters.UserSuccessResponse(result)) + } +} \ No newline at end of file diff --git a/api/handlers/user_referral.go b/api/handlers/user_referral.go new file mode 100644 index 0000000..961bc65 --- /dev/null +++ b/api/handlers/user_referral.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "gss-backend/api/presenters" + services "gss-backend/pkg/services/user_referral" + + "github.com/gofiber/fiber/v2" +) + + +func FindLeaderboardScores(userReferralService services.IUserReferralService) fiber.Handler { + return func(c *fiber.Ctx) error { + result, err := userReferralService.FindLeaderboardScores() + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenters.UserReferralErrorResponse(err)) + } + + return c.JSON(presenters.LeaderboardScoreSuceessResponse(result)) + } +} + diff --git a/api/presenters/user.go b/api/presenters/user.go new file mode 100644 index 0000000..00fd2b0 --- /dev/null +++ b/api/presenters/user.go @@ -0,0 +1,57 @@ +package presenters + +import ( + "gss-backend/pkg/models" + + "github.com/gofiber/fiber/v2" +) + +// UserPresenter is a struct that is going to be used to present the user data in a controlled way +// As a minor "business logic", I opted to not return the phone number and the email of the user +type UserPresenter struct { + ID uint `json:"id"` + FullName string `json:"full_name"` + ReferralCode string `json:"referral_code"` +} + +// Functions to transform the user 'raw' data to presenters +func UserSuccessResponse(data *models.User) *fiber.Map { + user := UserPresenter{ + ID: data.ID, + FullName: data.FullName, + ReferralCode: data.ReferralCode, + + } + + return &fiber.Map{ + "status": "success", + "data": user, + "error": nil, + } +} + +func UsersSuccessResponse(data *[]models.User) *fiber.Map { + var users []UserPresenter + + for _, user := range *data { + users = append(users, UserPresenter{ + ID: user.ID, + FullName: user.FullName, + ReferralCode: user.ReferralCode, + }) + } + + return &fiber.Map{ + "status": "success", + "data": users, + "error": nil, + } +} + +func UserErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": "error", + "data": nil, + "error": err.Error(), + } +} \ No newline at end of file diff --git a/api/presenters/user_referral.go b/api/presenters/user_referral.go new file mode 100644 index 0000000..4305bcf --- /dev/null +++ b/api/presenters/user_referral.go @@ -0,0 +1,43 @@ +package presenters + +import ( + repo "gss-backend/pkg/repositories/user_referral" + + "github.com/gofiber/fiber/v2" +) + +type PointsPresenter struct { + ID uint `json:"id"` + Points uint `json:"points"` +} + +type LeaderboardScorePresenter struct { + ReferrerId uint `json:"referrer_id"` + FullName string `json:"full_name"` + ReferralsCount uint `json:"referrals_count"` +} + +func LeaderboardScoreSuceessResponse(data *[]repo.LeaderboardScore) *fiber.Map { + var leaderboardScores []LeaderboardScorePresenter + + for _, score := range *data { + leaderboardScores = append(leaderboardScores, LeaderboardScorePresenter{ + ReferrerId: score.ReferrerId, + FullName: score.FullName, + ReferralsCount: score.ReferralsCount, + }) + } + return &fiber.Map{ + "status": "success", + "data": leaderboardScores, + "error": nil, + } +} + +func UserReferralErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": "error", + "data": nil, + "error": err.Error(), + } +} \ No newline at end of file diff --git a/api/routes/user.go b/api/routes/user.go new file mode 100644 index 0000000..2c2e5aa --- /dev/null +++ b/api/routes/user.go @@ -0,0 +1,15 @@ +package routes + +import ( + "gss-backend/api/handlers" + services "gss-backend/pkg/services/user" + + "github.com/gofiber/fiber/v2" +) + +// UserRouter is a function created to define User related routes +func UserRouter(app fiber.Router, user_service services.IUserService) { + app.Post("/users", handlers.CreateUser(user_service)) + app.Get("/users", handlers.FindAllUsers(user_service)) + app.Get("/users/:id", handlers.FindUserByID(user_service)) +} \ No newline at end of file diff --git a/api/routes/user_referral.go b/api/routes/user_referral.go new file mode 100644 index 0000000..0492575 --- /dev/null +++ b/api/routes/user_referral.go @@ -0,0 +1,13 @@ +package routes + +import ( + "gss-backend/api/handlers" + services "gss-backend/pkg/services/user_referral" + + "github.com/gofiber/fiber/v2" +) + + +func UserReferralRouter(app fiber.Router, user_referral_service services.IUserReferralService) { + app.Get("/leaderboard", handlers.FindLeaderboardScores(user_referral_service)) +} \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..baeeaa7 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "gss-backend/api/routes" + "gss-backend/pkg/config" + "gss-backend/pkg/models" + emailService "gss-backend/pkg/services/email" + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func main() { + // Loading configuration + log.Println("Loading configuration...") + config, err := config.NewConfig() + + if err != nil { + log.Fatal(err) + } + + log.Println("Configuration loaded!") + + // Setting up Postgres DSN + dsn := fmt.Sprintf(( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=UTC"), + config.POSTGRES_HOST, + config.POSTGRES_PORT, + config.POSTGRES_USER, + config.POSTGRES_PASSWORD, + config.POSTGRES_DB, + ) + + // Connecting to the database + log.Println("Connecting to the database...") + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + + if err != nil { + log.Fatal(err) + } + + log.Println("Connected to the database!") + + // Setting up Migrations + log.Println("Running migrations...") + + err = db.AutoMigrate(&models.User{}, &models.UserReferral{}) + + if err != nil { + log.Fatal("Error running migrations", err) + } + + log.Println("Migrations complete!") + + // Setting up Fiber + app := fiber.New() + app.Use(cors.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("GSS Gateway API is up and running! 🚀") + }) + + // Instatiating Repositories + repoContainer := NewRepositoryContainer(db) + + + // Instatiating Services + emailConfig := emailService.EmailConfig{ + SMTPHost: config.SMTP_HOST, + SMTPPort: config.SMTP_PORT, + SMTPEmail: config.SMTP_EMAIL, + SMTPPassword: config.SMTP_PASSWORD, + } + serviceContainer := NewServiceContainer(repoContainer, emailConfig) + + // Setting up routes + api := app.Group("/api") + routes.UserRouter(api, serviceContainer.UserService) + routes.UserReferralRouter(api, serviceContainer.UserReferralService) + + // Starting the server + port := fmt.Sprintf(":%s", config.FIBER_PORT) + log.Fatal(app.Listen(port)) + + +} \ No newline at end of file diff --git a/cmd/repository_container.go b/cmd/repository_container.go new file mode 100644 index 0000000..22db8dd --- /dev/null +++ b/cmd/repository_container.go @@ -0,0 +1,20 @@ +package main + +import ( + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + + "gorm.io/gorm" +) + +type RepositoryContainer struct { + UserRepository userRepo.IUserRepository + UserReferralRepository userReferralRepo.IUserReferralRepository +} + +func NewRepositoryContainer(db *gorm.DB) *RepositoryContainer { + return &RepositoryContainer{ + UserRepository: userRepo.NewPostgresUserRepository(db), + UserReferralRepository: userReferralRepo.NewPostgresUserReferralRepository(db), + } +} diff --git a/cmd/service_container.go b/cmd/service_container.go new file mode 100644 index 0000000..72dd017 --- /dev/null +++ b/cmd/service_container.go @@ -0,0 +1,26 @@ +package main + +import ( + emailService "gss-backend/pkg/services/email" + userService "gss-backend/pkg/services/user" + userReferralService "gss-backend/pkg/services/user_referral" +) + + +type ServiceContainer struct { + EmailService emailService.IEmailService + UserService userService.IUserService + UserReferralService userReferralService.IUserReferralService +} + +func NewServiceContainer(repoContainer *RepositoryContainer, emailConfig emailService.EmailConfig) *ServiceContainer { + emailService := emailService.NewEmailService(emailConfig) + userService := userService.NewUserService(repoContainer.UserRepository, repoContainer.UserReferralRepository, emailService) + userReferralService := userReferralService.NewUserReferralService(repoContainer.UserRepository, repoContainer.UserReferralRepository, emailService) + + return &ServiceContainer{ + EmailService: emailService, + UserService: userService, + UserReferralService: userReferralService, + } +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2d34f9e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,42 @@ +services: + gss-api: + container_name: gss-api + build: + context: . + dockerfile: Dockerfile + ports: + - "3001:3001" + restart: always + env_file: + - .env.production + depends_on: + gss-postgres-db: + condition: service_healthy + networks: + - gss-network + + gss-postgres-db: + container_name: gss-postgres-db + image: postgres:13 + ports: + - "5432:5432" + restart: always + env_file: + - .env.production + volumes: + - ./volumes/gss-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - gss-network + +volumes: + gss-db: + driver: local + +networks: + gss-network: + driver: bridge diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e46d689 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module gss-backend + +go 1.24.1 + +require ( + github.com/gofiber/fiber/v2 v2.52.6 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + gorm.io/driver/postgres v1.5.11 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.23.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/mail.v2 v2.3.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a1573d8 --- /dev/null +++ b/go.sum @@ -0,0 +1,69 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= +github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..2d07e7d --- /dev/null +++ b/notes.txt @@ -0,0 +1 @@ +docker run --name gss-dev-db -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=gss-db -p 5432:5432 -d postgres:13 \ No newline at end of file diff --git a/pkg/config/env.go b/pkg/config/env.go new file mode 100644 index 0000000..ac14d44 --- /dev/null +++ b/pkg/config/env.go @@ -0,0 +1,63 @@ +package config + +import ( + "log" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +// Config struct to abstract the environment variables +type Config struct { + POSTGRES_HOST string + POSTGRES_PORT int + POSTGRES_USER string + POSTGRES_PASSWORD string + POSTGRES_DB string + FIBER_PORT string + SMTP_EMAIL string + SMTP_HOST string + SMTP_PORT int + SMTP_USER string + SMTP_PASSWORD string +} + +func NewConfig() (*Config, error) { + err := godotenv.Load() + + if err != nil { + log.Fatal("Error loading .env file") + } + config := &Config{ + POSTGRES_HOST: getEnv("POSTGRES_HOST", "localhost"), + POSTGRES_PORT: getEnvAsInt("POSTGRES_PORT", 5432), + POSTGRES_USER: getEnv("POSTGRES_USER", "root"), + POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), + POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), + FIBER_PORT: getEnv("PORT", "3000"), + SMTP_EMAIL: getEnv("SMTP_EMAIL", ""), + SMTP_PORT: getEnvAsInt("SMTP_PORT", 587), + SMTP_HOST: getEnv("SMTP_HOST", ""), + SMTP_PASSWORD: getEnv("SMTP_PASSWORD", ""), + + } + + return config, nil +} + +// Auxiliar functions to get the environment variables +func getEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} + +func getEnvAsInt(name string, defaultValue int) int { + valueStr := getEnv(name, "") + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + return defaultValue +} diff --git a/pkg/models/user.go b/pkg/models/user.go new file mode 100644 index 0000000..35f1fa5 --- /dev/null +++ b/pkg/models/user.go @@ -0,0 +1,15 @@ +package models + +import ( + "gorm.io/gorm" +) + +// User struct to model the user entity +type User struct { + gorm.Model + ID uint `json:"id" gorm:"primary_key"` + FullName string `json:"full_name" gor:"not null"` + Email string `json:"email" gorm:"not null; unique"` + PhoneNumber string `json:"phone_number" gorm:"not null"` + ReferralCode string `json:"referral_code" gorm:"not null; unique"` +} \ No newline at end of file diff --git a/pkg/models/user_referral.go b/pkg/models/user_referral.go new file mode 100644 index 0000000..90a9bc5 --- /dev/null +++ b/pkg/models/user_referral.go @@ -0,0 +1,15 @@ +package models + +import ( + "gorm.io/gorm" +) + +// Struct that represents the connection +type UserReferral struct { + gorm.Model + ID uint `json:"id" gorm:"primary_key"` + ReferrerId uint `json:"referrer_id" gorm:"not null"` // The user that referred the other user + Referrer User `gorm:"foreignKey:ReferrerId; constraint:OnDelete:CASCADE"` + ReferredId uint `json:"referred_id" gorm:"not null"` // The user that was referred + Referred User `gorm:"foreignKey:ReferredId; constraint:OnDelete:CASCADE"` +} \ No newline at end of file diff --git a/pkg/repositories/user/interfaces.go b/pkg/repositories/user/interfaces.go new file mode 100644 index 0000000..01f728a --- /dev/null +++ b/pkg/repositories/user/interfaces.go @@ -0,0 +1,20 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +// Interfaces that I going to use to implement the repository pattern +type IUserRepository interface { + FindAll() (*[]models.User, error) + FindByID(id uint) (*models.User, error) + FindByReferralCode(referralCode string) (*models.User, error) + FindByEmail(email string) (*models.User, error) + Create(user *models.User) (*models.User, error) +} + +type PostgresUserRepository struct { + db *gorm.DB +} \ No newline at end of file diff --git a/pkg/repositories/user/postgres_user_repository.go b/pkg/repositories/user/postgres_user_repository.go new file mode 100644 index 0000000..8428771 --- /dev/null +++ b/pkg/repositories/user/postgres_user_repository.go @@ -0,0 +1,41 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +// Concrete implementation of the IUserRepository interface +func NewPostgresUserRepository(db *gorm.DB) *PostgresUserRepository { + return &PostgresUserRepository{db: db} +} + +func (r *PostgresUserRepository) Create(user *models.User) (*models.User, error) { + result := r.db.Create(user) + return user, result.Error +} + +func (r *PostgresUserRepository) FindAll() (*[]models.User, error) { + var users []models.User + result := r.db.Find(&users) + return &users, result.Error +} + +func (r *PostgresUserRepository) FindByID(id uint) (*models.User, error) { + var user models.User + result := r.db.First(&user, id) + return &user, result.Error +} + +func (r *PostgresUserRepository) FindByEmail(email string) (*models.User, error) { + var user models.User + result := r.db.Where("email = ?", email).First(&user) + return &user, result.Error +} + +func (r *PostgresUserRepository) FindByReferralCode(referralCode string) (*models.User, error) { + var user models.User + result := r.db.Where("referral_code = ?", referralCode).First(&user) + return &user, result.Error +} \ No newline at end of file diff --git a/pkg/repositories/user_referral/interfaces.go b/pkg/repositories/user_referral/interfaces.go new file mode 100644 index 0000000..68e6d4e --- /dev/null +++ b/pkg/repositories/user_referral/interfaces.go @@ -0,0 +1,22 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +type IUserReferralRepository interface { + Create(referrerId uint, referredId uint) (*models.UserReferral, error) + FindLeaderboard() (*[]LeaderboardScore, error) +} + +type PostgresUserReferralRepository struct { + db *gorm.DB +} + +type LeaderboardScore struct { + ReferrerId uint + FullName string + ReferralsCount uint +} \ No newline at end of file diff --git a/pkg/repositories/user_referral/postgres_user_referral_repository.go b/pkg/repositories/user_referral/postgres_user_referral_repository.go new file mode 100644 index 0000000..4d2080b --- /dev/null +++ b/pkg/repositories/user_referral/postgres_user_referral_repository.go @@ -0,0 +1,37 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +// Concrete implementation of the IPointsRepository interface +func NewPostgresUserReferralRepository(db *gorm.DB) *PostgresUserReferralRepository { + return &PostgresUserReferralRepository{db: db} +} + +// Create a new user referral +func (r *PostgresUserReferralRepository) Create(referrerId uint, referredId uint) (*models.UserReferral, error) { + userReferral := models.UserReferral{ReferrerId: referrerId, ReferredId: referredId} + result := r.db.Create(&userReferral) + return &userReferral, result.Error + +} + +// Find the top 10 users with the most referrals +func (r *PostgresUserReferralRepository) FindLeaderboard() (*[]LeaderboardScore, error) { + var leaderboardScores []LeaderboardScore + + result := r.db.Table("user_referrals"). + Select("user_referrals.referrer_id, users.full_name, COUNT(user_referrals.referrer_id) as referrals_count"). + Joins("JOIN users ON users.id = user_referrals.referrer_id"). + Group("user_referrals.referrer_id, users.full_name"). + Order("referrals_count DESC"). + Limit(10). + Find(&leaderboardScores) + + + return &leaderboardScores, result.Error +} + diff --git a/pkg/services/email/email_service.go b/pkg/services/email/email_service.go new file mode 100644 index 0000000..0053cc4 --- /dev/null +++ b/pkg/services/email/email_service.go @@ -0,0 +1,87 @@ +package services + +import ( + gomail "gopkg.in/mail.v2" +) + +// Instantiate a new EmailService +func NewEmailService(emailConfig EmailConfig) *EmailService { + return &EmailService{ + EmailConfig: emailConfig, + } +} + +// Send a welcome email to the user upon registration +func (s *EmailService) SendWelcomeEmail(email string) error { + // Creating a new message + message := gomail.NewMessage() + + // Setting email headers + message.SetHeader("From", s.EmailConfig.SMTPEmail) + message.SetHeader("To", email) + message.SetHeader("Subject", "Welcome to GSS Eco News!") + + // Setting email body + message.SetBody("text/plain", "Welcome to GSS Eco News! We are excited to have you on board!") + + // Setting up SMTP configuration + dialer := gomail.NewDialer( + s.EmailConfig.SMTPHost, + s.EmailConfig.SMTPPort, + s.EmailConfig.SMTPEmail, + s.EmailConfig.SMTPPassword, + ) + + // Sending the email + return dialer.DialAndSend(message) +} + +// Send email to user when someones is registered using their referral link +func (s *EmailService) SendReferralLinkAccess(email string) error { + // Creating a new message + message := gomail.NewMessage() + + // Setting email headers + message.SetHeader("From", s.EmailConfig.SMTPEmail) + message.SetHeader("To", email) + message.SetHeader("Subject", "Someone has registered using your referral link!") + + // Setting email body + message.SetBody("text/plain", "Someone has registered using your referral link! You have earned 1 point in the competition!") + + // Setting up SMTP configuration + dialer := gomail.NewDialer( + s.EmailConfig.SMTPHost, + s.EmailConfig.SMTPPort, + s.EmailConfig.SMTPEmail, + s.EmailConfig.SMTPPassword, + ) + + // Sending the email + return dialer.DialAndSend(message) +} + +// Send email to user when they are in the top 10 of the leaderboard +func (s *EmailService) SendLeaderboardEmail(email string) error { + // Creating a new message + message := gomail.NewMessage() + + // Setting email headers + message.SetHeader("From", s.EmailConfig.SMTPEmail) + message.SetHeader("To", email) + message.SetHeader("Subject", "Congratulations! You are in the top 10 of the leaderboard!") + + // Setting email body + message.SetBody("text/plain", "Congratulations! You are in the top 10 of the leaderboard! Thanks for participating the competition!") + + // Setting up SMTP configuration + dialer := gomail.NewDialer( + s.EmailConfig.SMTPHost, + s.EmailConfig.SMTPPort, + s.EmailConfig.SMTPEmail, + s.EmailConfig.SMTPPassword, + ) + + // Sending the email + return dialer.DialAndSend(message) +} \ No newline at end of file diff --git a/pkg/services/email/interfaces.go b/pkg/services/email/interfaces.go new file mode 100644 index 0000000..22ace25 --- /dev/null +++ b/pkg/services/email/interfaces.go @@ -0,0 +1,19 @@ +package services + + +type IEmailService interface { + SendWelcomeEmail(email string) error + SendReferralLinkAccess(email string) error + SendLeaderboardEmail(email string) error +} + +type EmailConfig struct { + SMTPEmail string + SMTPHost string + SMTPPort int + SMTPPassword string +} + +type EmailService struct { + EmailConfig EmailConfig +} diff --git a/pkg/services/user/interfaces.go b/pkg/services/user/interfaces.go new file mode 100644 index 0000000..6e7b4ad --- /dev/null +++ b/pkg/services/user/interfaces.go @@ -0,0 +1,22 @@ +package services + +import ( + "gss-backend/api/dtos" + "gss-backend/pkg/models" + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" +) + +type IUserService interface { + FindAll() (*[]models.User, error) + FindByID(id uint) (*models.User, error) + FindByReferralCode(referralCode string) (*models.User, error) + Create(userDto *dtos.CreateUserDTO) (*models.User, error) +} + +type UserService struct { + userRepo userRepo.IUserRepository + userReferralRepo userReferralRepo.IUserReferralRepository + emailService emailService.IEmailService +} \ No newline at end of file diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go new file mode 100644 index 0000000..6233cb6 --- /dev/null +++ b/pkg/services/user/user_service.go @@ -0,0 +1,116 @@ +package services + +import ( + "fmt" + "gss-backend/api/dtos" + "gss-backend/pkg/models" + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" + "gss-backend/pkg/utils" +) + +// Instatiate a new UserService +func NewUserService( + userRepo userRepo.IUserRepository, + userReferralRepo userReferralRepo.IUserReferralRepository, + emailService emailService.IEmailService ) *UserService { + + return &UserService{ + userRepo: userRepo, + userReferralRepo: userReferralRepo, + emailService: emailService, + + } +} + +// Register a new user and create a new points record for the user +func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) { + // Check if user already exists + user, err := s.userRepo.FindByEmail(userDto.Email) + + if err == nil && user != nil { + return nil, fmt.Errorf("user with email %s already exists", userDto.Email) + } + + // Generate new referral code for the user + referralCode := utils.GenerateReferralCode() + + + // Create a new user model + newUser := models.User{ + FullName: userDto.FullName, + Email: userDto.Email, + PhoneNumber: userDto.PhoneNumber, + ReferralCode: referralCode, + } + + // Create a new user record in the database + createdUser, err := s.userRepo.Create(&newUser) + + if err != nil { + return nil, err + } + + // Send welcome email to the user asynchronously + go func() { + err := s.emailService.SendWelcomeEmail(createdUser.Email) + + if err != nil { + fmt.Printf("Failed to send welcome email to %s: %v\n", createdUser.Email, err) + } + }() + + + // Create user referral record for the registered user + _, err = s.userReferralRepo.Create(createdUser.ID, createdUser.ID) + + if err != nil { + return nil, err + } + + // If user has a referral code, set another record for the referrer + if userDto.ReferrerCode != "" { + referrerUser, err := s.userRepo.FindByReferralCode(userDto.ReferrerCode) + + if err != nil { + return nil, err + } + + _, err = s.userReferralRepo.Create(referrerUser.ID, createdUser.ID) + + if err != nil { + return nil, err + } + + // Send email to the referrer user asynchronously + go func() { + // Rewrote in another way so I can remind this also works hehe + if err := s.emailService.SendReferralLinkAccess(referrerUser.Email); err != nil { + fmt.Printf("Error sending referral link access email to %s: %v\n", referrerUser.Email, err) + } + }() + } + + return createdUser, nil +} + +// Find all users (used for developing purposes) +func (s *UserService) FindAll() (*[]models.User, error) { + return s.userRepo.FindAll() +} + +// Find a user by their ID (used for developing purposes) +func (s *UserService) FindByID(id uint) (*models.User, error) { + return s.userRepo.FindByID(id) +} + +// Find a user by their referral code +func (s *UserService) FindByReferralCode(referralCode string) (*models.User, error) { + return s.userRepo.FindByReferralCode(referralCode) +} + + + + + diff --git a/pkg/services/user_referral/interfaces.go b/pkg/services/user_referral/interfaces.go new file mode 100644 index 0000000..7c864d5 --- /dev/null +++ b/pkg/services/user_referral/interfaces.go @@ -0,0 +1,17 @@ +package services + +import ( + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" +) + +type IUserReferralService interface { + FindLeaderboardScores() (*[]userReferralRepo.LeaderboardScore, error) +} + +type UserReferralService struct { + userReferralRepo userReferralRepo.IUserReferralRepository + userRepo userRepo.IUserRepository + emailService emailService.IEmailService +} \ No newline at end of file diff --git a/pkg/services/user_referral/user_referral_service.go b/pkg/services/user_referral/user_referral_service.go new file mode 100644 index 0000000..0c06d29 --- /dev/null +++ b/pkg/services/user_referral/user_referral_service.go @@ -0,0 +1,70 @@ +package services + +import ( + "fmt" + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" +) + + + +func NewUserReferralService(userRepo userRepo.IUserRepository, + userReferralRepo userReferralRepo.IUserReferralRepository, + emailService emailService.IEmailService) IUserReferralService { + return &UserReferralService{ + userRepo: userRepo, + userReferralRepo: userReferralRepo, + emailService: emailService, + } +} + +// Function that calculates the appropiate score for the leaderboard +func (s *UserReferralService) FindLeaderboardScores() (*[]userReferralRepo.LeaderboardScore, error) { + // Find the top 10 users with the most referrals + leaderboardScores, err := s.userReferralRepo.FindLeaderboard() + + if err != nil { + return nil, err + } + + // Instatiating error channel + errChan := make(chan error, len(*leaderboardScores)) + + // For every user in the leaderboard, send an email to say he is in the leaderboard + for _, leaderboardScore := range *leaderboardScores { + // Get user data and send email asynchronously + // If any step causes an error, send the error to the error channel + go func(leaderboardScore userReferralRepo.LeaderboardScore) { + user, err := s.userRepo.FindByID(leaderboardScore.ReferrerId) + + if err != nil { + errChan <- fmt.Errorf("Failed to find user with ID %d: %v", leaderboardScore.ReferrerId, err) + return + } + + err = s.emailService.SendLeaderboardEmail(user.Email) + + if err != nil { + errChan <- fmt.Errorf("Failed to send leaderboard email to %s: %v", user.Email, err) + return + } + + errChan <- nil // Signal that the email was sent successfully + }(leaderboardScore) + + } + + // Processing errors in another goroutine + go func() { + for range *leaderboardScores { + if err := <-errChan; err != nil { + fmt.Println("Error processing leaderboard email: ", err) + } + } + + close(errChan) + }() + + return leaderboardScores, nil +} diff --git a/pkg/utils/generate_referral_code .go b/pkg/utils/generate_referral_code .go new file mode 100644 index 0000000..ade3080 --- /dev/null +++ b/pkg/utils/generate_referral_code .go @@ -0,0 +1,7 @@ +package utils + +import "github.com/google/uuid" + +func GenerateReferralCode() string { + return uuid.NewString() +} \ No newline at end of file