diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0cf0326 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +DB_HOST= +DB_USER= +DB_PASS= +DB_NAME= +DB_PORT= + +FRONTEND_URL= + +SMTP_HOST=sandbox.smtp.mailtrap.io +SMTP_PORT=2525 +SMTP_EMAIL= +SMTP_PASSWORD= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acca4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of https://www.toptal.com/developers/gitignore/api/go +.env +.air.toml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9561468 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.23-alpine AS builder +RUN apk add --no-cache git + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o main . + +FROM alpine:latest +WORKDIR /root/ +COPY --from=builder /app/main . +EXPOSE 8080 + +CMD ["./main"] diff --git a/Insomnia_2025-04-02.har b/Insomnia_2025-04-02.har new file mode 100644 index 0000000..bb79b52 --- /dev/null +++ b/Insomnia_2025-04-02.har @@ -0,0 +1,201 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Insomnia REST Client", + "version": "insomnia.desktop.app:v11.0.1" + }, + "entries": [ + { + "startedDateTime": "2025-04-02T22:10:32.798Z", + "time": 2.291, + "request": { + "method": "GET", + "url": "http://localhost:8080/api/winners", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "User-Agent", + "value": "insomnia/11.0.1" + } + ], + "queryString": [], + "postData": { + "mimeType": "", + "text": "" + }, + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Date", + "value": "Wed, 02 Apr 2025 22:09:45 GMT" + }, + { + "name": "Content-Length", + "value": "2" + } + ], + "content": { + "size": 2, + "mimeType": "application/json; charset=utf-8", + "text": "[]" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": 0, + "wait": 2.291, + "receive": 0, + "ssl": -1 + }, + "comment": "Listar os vencedores da competição" + }, + { + "startedDateTime": "2025-04-02T22:10:32.798Z", + "time": 2.706, + "request": { + "method": "POST", + "url": "http://localhost:8080/api/end-competition", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "User-Agent", + "value": "insomnia/11.0.1" + } + ], + "queryString": [], + "postData": { + "mimeType": "", + "text": "" + }, + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Date", + "value": "Wed, 02 Apr 2025 22:09:50 GMT" + }, + { + "name": "Content-Length", + "value": "60" + } + ], + "content": { + "size": 60, + "mimeType": "application/json; charset=utf-8", + "text": "{\"message\":\"Competition ended, winners have been notified!\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": 0, + "wait": 2.706, + "receive": 0, + "ssl": -1 + }, + "comment": "Finalizar Competição" + }, + { + "startedDateTime": "2025-04-02T22:10:32.798Z", + "time": 4140.493, + "request": { + "method": "POST", + "url": "http://localhost:8080/api/signup", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "User-Agent", + "value": "insomnia/11.0.1" + } + ], + "queryString": [], + "postData": { + "mimeType": "application/json", + "text": "{\n\t\t\"name\": \"t harumi\",\n \"email\": \"test@email.com\",\n\t \"phone_number\": \"12342256789\",\n\t\t\"referred_by\": \"33ba65a2-7ea8-4118-880b-7f560e42d4ce\"\n}" + }, + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 201, + "statusText": "Created", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Date", + "value": "Wed, 02 Apr 2025 22:07:07 GMT" + }, + { + "name": "Content-Length", + "value": "177" + } + ], + "content": { + "size": 177, + "mimeType": "application/json; charset=utf-8", + "text": "{\"message\":\"User registered successfully\",\"referral_code\":\"b284f7f9-d062-43aa-b20e-f7a34dc0a949\",\"share_link\":\"https://test.com/signup?ref=b284f7f9-d062-43aa-b20e-f7a34dc0a949\"}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": 0, + "wait": 4140.493, + "receive": 0, + "ssl": -1 + }, + "comment": "Cadastrar Usuário" + } + ] + } +} \ No newline at end of file diff --git a/Insomnia_2025-04-02.yaml b/Insomnia_2025-04-02.yaml new file mode 100644 index 0000000..43be2c7 --- /dev/null +++ b/Insomnia_2025-04-02.yaml @@ -0,0 +1,91 @@ +type: collection.insomnia.rest/5.0 +name: BACK-END-TEST-COMPETITION +meta: + id: wrk_b13d67c651604252b1d7f224c975c901 + created: 1743630557310 + modified: 1743630557310 +collection: + - url: http://localhost:8080/api/signup + name: Cadastrar Usuário + meta: + id: req_5acf0c81419d4b12ae35bd5a1c7fbcfb + created: 1743630565594 + modified: 1743631623100 + isPrivate: false + sortKey: -1743630565594 + method: POST + body: + mimeType: application/json + text: |- + { + "name": "name", + "email": "test@email.com", + "phone_number": "12342256789", + "referred_by": "" + } + headers: + - name: Content-Type + value: application/json + - name: User-Agent + value: insomnia/11.0.1 + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true + - url: http://localhost:8080/api/end-competition + name: Finalizar Competição + meta: + id: req_01fbead0b8034164a6bbcbb56118b836 + created: 1743630612973 + modified: 1743631681617 + isPrivate: false + sortKey: -1743630612973 + method: POST + headers: + - name: User-Agent + value: insomnia/11.0.1 + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true + - url: http://localhost:8080/api/winners + name: Listar os vencedores da competição + meta: + id: req_8c0627869b614988aa79b4d007fe32e3 + created: 1743630630346 + modified: 1743631785681 + isPrivate: false + sortKey: -1743630630346 + method: GET + headers: + - name: User-Agent + value: insomnia/11.0.1 + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true +cookieJar: + name: Default Jar + meta: + id: jar_130abd7a0ddc52cf3e136adf08dbcf2dbdca2d00 + created: 1743630557314 + modified: 1743630557314 +environments: + name: Base Environment + meta: + id: env_130abd7a0ddc52cf3e136adf08dbcf2dbdca2d00 + created: 1743630557312 + modified: 1743630557312 + isPrivate: false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aa1a0a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: build run air stop logs db-up db-down clean + +# Nome do container do banco de dados +DB_CONTAINER = postgres_competition + +# 🔹 Subir apenas o banco de dados com docker-compose +db-up: + docker-compose up -d db + +# 🔹 Parar e remover o banco de dados +db-down: + docker-compose down + +# 🔹 Rodar a aplicação Go normalmente +run: + go run main.go + +# 🔹 Rodar a aplicação com live reload (precisa do Air instalado) +air: + air + +# 🔹 Parar o banco de dados e limpar containers +stop: + docker stop $(DB_CONTAINER) || true + docker rm $(DB_CONTAINER) || true + +# 🔹 Ver logs do banco de dados +logs: + docker logs -f $(DB_CONTAINER) + +# 🔹 Limpar volumes e imagens não usadas +clean: stop db-down + docker volume rm $(shell docker volume ls -q) || true diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..495cffa --- /dev/null +++ b/config/config.go @@ -0,0 +1,17 @@ +package config + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func LoadEnv() { + if os.Getenv("RENDER") == "" { + err := godotenv.Load() + if err != nil { + log.Println("Aviso: Nenhum arquivo .env encontrado. Usando variáveis de ambiente do sistema.") + } + } +} \ No newline at end of file diff --git a/config/cors.go b/config/cors.go new file mode 100644 index 0000000..08939ab --- /dev/null +++ b/config/cors.go @@ -0,0 +1,24 @@ +package config + +import ( + "log" + "os" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func CORSMiddleware() gin.HandlerFunc { + frontendURL := os.Getenv("FRONTEND_URL") + + log.Println("CORS AllowOrigins:", frontendURL) + + config := cors.Config{ + AllowOrigins: []string{frontendURL}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-Requested-With"}, + AllowCredentials: true, + } + + return cors.New(config) +} diff --git a/config/database.go b/config/database.go new file mode 100644 index 0000000..778f8df --- /dev/null +++ b/config/database.go @@ -0,0 +1,31 @@ +package config + +import ( + "fmt" + "log" + "os" + + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func ConnectDatabase() { + dsn := fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + os.Getenv("DB_HOST"), + os.Getenv("DB_USER"), + os.Getenv("DB_PASS"), + os.Getenv("DB_NAME"), + os.Getenv("DB_PORT"), + ) + + database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + DB = database + fmt.Println("Database connected successfully") +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7f9d070 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.8" + +services: + db: + image: postgres:latest + container_name: postgres_competition + restart: always + env_file: + - .env + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASS} + POSTGRES_DB: ${DB_NAME} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..62f1df0 --- /dev/null +++ b/go.mod @@ -0,0 +1,63 @@ +module github.com/jpeccia/go-backend-test + +go 1.23.4 + +require github.com/google/uuid v1.6.0 + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/cors v1.7.4 + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/urfave/cli/v2 v2.27.6 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.31.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.11 // indirect + gorm.io/gorm v1.25.12 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2440ea8 --- /dev/null +++ b/go.sum @@ -0,0 +1,171 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/cors v1.7.4 h1:/fC6/wk7rCRtqKqki8lLr2Xq+hnV49aXDLIuSek9g4k= +github.com/gin-contrib/cors v1.7.4/go.mod h1:vGc/APSgLMlQfEJV5NAzkrAHb0C8DetL3K6QZuvGii0= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/database/migrations/migrate.go b/internal/database/migrations/migrate.go new file mode 100644 index 0000000..243ddc1 --- /dev/null +++ b/internal/database/migrations/migrate.go @@ -0,0 +1,21 @@ +package migrations + +import ( + "log" + + "github.com/jpeccia/go-backend-test/config" + "github.com/jpeccia/go-backend-test/internal/models" +) + +func Migrate() { + db := config.DB + + log.Println("Running database migrations...") + + err := db.AutoMigrate(&models.User{}) + if err != nil { + log.Fatalf("Migration failed: %v", err) + } + + log.Println("Database migrated successfully!") +} diff --git a/internal/dto/user_dto.go b/internal/dto/user_dto.go new file mode 100644 index 0000000..3aef727 --- /dev/null +++ b/internal/dto/user_dto.go @@ -0,0 +1,8 @@ +package dto + +type RegisterUserDTO struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` + PhoneNumber string `json:"phone_number" binding:"required"` + ReferredBy string `json:"referred_by,omitempty"` +} \ No newline at end of file diff --git a/internal/handlers/competition_handler.go b/internal/handlers/competition_handler.go new file mode 100644 index 0000000..6412c2c --- /dev/null +++ b/internal/handlers/competition_handler.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/jpeccia/go-backend-test/internal/services" +) + +type CompetitionHandler struct { + competitionService services.CompetitionService +} + +func NewCompetitionHandler(service services.CompetitionService) *CompetitionHandler { + return &CompetitionHandler{competitionService: service} +} + +func (h *CompetitionHandler) GetWinners(c *gin.Context) { + winners, err := h.competitionService.GetTopWinners(10) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve winners"}) + return + } + + c.JSON(http.StatusOK, winners) +} + +func (h *CompetitionHandler) EndCompetition(c *gin.Context) { + winners, err := h.competitionService.GetTopWinners(10) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve winners"}) + return + } + + err = h.competitionService.NotifyWinners(winners) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send emails to winners"}) + } + + c.JSON(http.StatusOK, gin.H{"message": "Competition ended, winners have been notified!"}) +} diff --git a/internal/handlers/user_handler.go b/internal/handlers/user_handler.go new file mode 100644 index 0000000..3f6831f --- /dev/null +++ b/internal/handlers/user_handler.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "fmt" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/jpeccia/go-backend-test/internal/dto" + "github.com/jpeccia/go-backend-test/internal/services" +) + +type UserHandler struct { + userService services.UserService +} + +func NewUserHandler(userService services.UserService) *UserHandler { + return &UserHandler{userService} +} + +func (u *UserHandler) RegisterUser(c *gin.Context) { + var input dto.RegisterUserDTO + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + referralCode := c.Query("ref") + if referralCode != "" { + input.ReferredBy = referralCode + } + + user, err := u.userService.RegisterUser(input) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to register user"}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "User registered successfully", + "referral_code": user.ReferralCode, + "share_link": fmt.Sprintf("%s/signup?ref=%s", os.Getenv("FRONTEND_URL"), user.ReferralCode), + }) +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..e13a9f9 --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,13 @@ +package models + +import "gorm.io/gorm" + +type User struct { + gorm.Model + Name string `json:"name"` + Email string `json:"email" gorm:"unique"` + PhoneNumber string `json:"phone_number"` + ReferralCode string `json:"referral_code" gorm:"unique"` + ReferredBy string `json:"referred_by"` + Points int `json:"points"` +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go new file mode 100644 index 0000000..5773c56 --- /dev/null +++ b/internal/repositories/user_repository.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "github.com/jpeccia/go-backend-test/internal/models" + "gorm.io/gorm" +) + +type UserRepository interface { + CreateUser(user *models.User) error + FindUserByEmail(email string) (*models.User, error) + FindUserByReferralCode(code string) (*models.User, error) + FindTopUsersByPoints(limit int) ([]models.User, error) + UpdateUser(user *models.User) error +} + +type userRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db} +} + +func (u *userRepository) CreateUser(user *models.User) error { + return u.db.Create(user).Error +} + +func (u *userRepository) FindUserByEmail(email string) (*models.User, error) { + var user models.User + err := u.db.Where("email= ?", email).First(&user).Error + return &user, err +} + +func (u *userRepository) FindUserByReferralCode(code string) (*models.User, error) { + var user models.User + err := u.db.Where("referral_code = ?", code).First(&user).Error + return &user, err +} + +func (u *userRepository) FindTopUsersByPoints(limit int) ([]models.User, error) { + var users []models.User + err := u.db.Order("points DESC").Limit(limit).Find(&users).Error + return users, err +} + +func (u *userRepository) UpdateUser(user *models.User) error { + return u.db.Save(user).Error +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go new file mode 100644 index 0000000..9cf30c0 --- /dev/null +++ b/internal/routes/routes.go @@ -0,0 +1,15 @@ +package routes + +import ( + "github.com/gin-gonic/gin" + "github.com/jpeccia/go-backend-test/internal/handlers" +) + +func SetupRoutes(r *gin.Engine, userHandler *handlers.UserHandler, competitionHandler *handlers.CompetitionHandler) { + api := r.Group("/api") + { + api.POST("/signup", userHandler.RegisterUser) + api.POST("/end-competition", competitionHandler.EndCompetition) + api.GET("/winners", competitionHandler.GetWinners) // Nova rota para vencedores + } +} diff --git a/internal/services/competition_service.go b/internal/services/competition_service.go new file mode 100644 index 0000000..e91da67 --- /dev/null +++ b/internal/services/competition_service.go @@ -0,0 +1,42 @@ +package services + +import ( + "fmt" + "log" + + "github.com/jpeccia/go-backend-test/internal/models" + "github.com/jpeccia/go-backend-test/internal/repositories" +) + +type CompetitionService interface { + GetTopWinners(limit int) ([]models.User, error) + NotifyWinners(winners []models.User) error +} + +type competitionService struct { + userRepo repositories.UserRepository +} + +func NewCompetitionService(userRepo repositories.UserRepository) CompetitionService { + return &competitionService{userRepo: userRepo} +} + +func (s *competitionService) GetTopWinners(limit int) ([]models.User, error) { + return s.userRepo.FindTopUsersByPoints(limit) +} + +func (s *competitionService) NotifyWinners(winners []models.User) error { + emailService := NewEmailService() + + for _, winner := range winners { + subject := "Congratulations! You are a winner!" + body := fmt.Sprintf("Dear %s,\n\nYou are one of the top 10 winners of the competition! Congratulations!\n\nBest regards,\nCompetition Team", winner.Name) + + err := emailService.SendEmail(winner.Email, subject, body) + if err != nil { + log.Printf("Failed to send email to %s: %v", winner.Email, err) + } + } + + return nil +} \ No newline at end of file diff --git a/internal/services/email_service.go b/internal/services/email_service.go new file mode 100644 index 0000000..88ae332 --- /dev/null +++ b/internal/services/email_service.go @@ -0,0 +1,45 @@ +package services + +import ( + "fmt" + "net/smtp" + "os" +) + +type EmailService interface { + SendEmail(to string, subject string, body string) error +} + +type emailService struct { + smtpHost string + smtpPort string + sender string + password string +} + +func NewEmailService() EmailService { + return &emailService{ + smtpHost: os.Getenv("SMTP_HOST"), + smtpPort: os.Getenv("SMTP_PORT"), + sender: os.Getenv("SMTP_EMAIL"), + password: os.Getenv("SMTP_PASSWORD"), + } +} + +func (e *emailService) SendEmail(to string, subject string, body string) error { + auth := smtp.PlainAuth("", e.sender, e.password, e.smtpHost) + + // Adicionando cabeçalhos obrigatórios para evitar problemas de entrega + msg := []byte(fmt.Sprintf( + "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", + e.sender, to, subject, body, + )) + + err := smtp.SendMail(e.smtpHost+":"+e.smtpPort, auth, e.sender, []string{to}, msg) + if err != nil { + fmt.Printf("Error to send mail: %v", err) + return err + } + + return nil +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..c39f813 --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,83 @@ +package services + +import ( + "errors" + "fmt" + "os" + + "github.com/google/uuid" + "github.com/jpeccia/go-backend-test/internal/dto" + "github.com/jpeccia/go-backend-test/internal/models" + "github.com/jpeccia/go-backend-test/internal/repositories" +) + +type UserService interface { + RegisterUser(dto dto.RegisterUserDTO) (*models.User, error) +} + +type userService struct { + userRepo repositories.UserRepository +} + +func NewUserService(userRepo repositories.UserRepository) UserService { + return &userService{userRepo} +} + +func (u *userService) RegisterUser(dto dto.RegisterUserDTO) (*models.User, error) { + referralCode := uuid.New().String() + + user := models.User{ + Name: dto.Name, + Email: dto.Email, + PhoneNumber: dto.PhoneNumber, + ReferralCode: referralCode, + Points: 1, + } + + emailService := NewEmailService() + + if dto.ReferredBy != "" { + referredUser, err := u.userRepo.FindUserByReferralCode(dto.ReferredBy) + if err != nil { + return nil, errors.New("invalid referral code") + } + + referredUser.Points++ + err = u.userRepo.UpdateUser(referredUser) + if err != nil { + return nil, err + } + + user.ReferredBy = referredUser.Email + + go func() { + err := emailService.SendEmail( + referredUser.Email, + "Congratulations! You earned an extra point!", + fmt.Sprintf("A new person signed up using your referral link! You now have %d points.", referredUser.Points), + ) + if err != nil { + fmt.Println("Erro ao enviar e-mail para o referenciador:", err) + } + }() + } + + err := u.userRepo.CreateUser(&user) + if err != nil { + return nil, err + } + + go func() { + err := emailService.SendEmail( + user.Email, + "Welcome to the competition!", + fmt.Sprintf("Hi %s :), you have successfully registered! Share your link to earn points: %s/signup?ref=%s", + user.Name, os.Getenv("FRONTEND_URL"), user.ReferralCode), + ) + if err != nil { + fmt.Println("Erro ao enviar e-mail de boas-vindas:", err) + } + }() + + return &user, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d607d3f --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/jpeccia/go-backend-test/config" + "github.com/jpeccia/go-backend-test/internal/database/migrations" + "github.com/jpeccia/go-backend-test/internal/handlers" + "github.com/jpeccia/go-backend-test/internal/repositories" + "github.com/jpeccia/go-backend-test/internal/routes" + "github.com/jpeccia/go-backend-test/internal/services" +) + +func main() { + config.LoadEnv() + config.ConnectDatabase() + migrations.Migrate() + + r := gin.Default() + r.Use(config.CORSMiddleware()) + + userRepo := repositories.NewUserRepository(config.DB) + + userService := services.NewUserService(userRepo) + competitionService := services.NewCompetitionService(userRepo) + + userHandler := handlers.NewUserHandler(userService) + competitionHandler := handlers.NewCompetitionHandler(competitionService) + + routes.SetupRoutes(r, userHandler, competitionHandler) + + r.Run(":8080") +} diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..0be2c41 --- /dev/null +++ b/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file