diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f36d5dd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+# 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
+go.work.sum
+
+# env file
+.env
+
+# tmp files
+tmp/
+
+#React gitignore
+
+.idea/
+.vscode/
+node_modules/
+build
+.DS_Store
+*.tgz
+my-app*
+template/src/__tests__/__snapshots__/
+lerna-debug.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+/.changelog
+.npm/
+yarn.lock
\ No newline at end of file
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..33a9ec2
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,10 @@
+FROM golang:1.24.1-alpine3.21
+
+WORKDIR /app
+
+RUN go install github.com/air-verse/air@latest
+
+COPY . .
+RUN go mod download
+
+CMD ["air", "run"]
\ No newline at end of file
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..44731e6
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,265 @@
+# 🏭 Carbon Offset API
+
+
+
+## 💻 Requirements:
+
+### `Go >= 1.24.1`
+
+### [`Docker`](https://www.docker.com/) & [`Docker compose`](https://docs.docker.com/compose/)
+
+## 📜 Documentation:
+
+`/users/`
+
+
+POST /users/
+
+- **Description**: Creates a new user.
+
+- **Headers**:
+
+ ```plaintext
+ Content-Type: application/json
+
+ ```
+
+- **Request Body**:
+
+ ```json
+ {
+ "name": "john",
+ "email": "john@gmail.com",
+ "phone": "+5519912345678"
+ }
+ ```
+
+- **Success Response (201 Created)**:
+
+ ```json
+ {
+ "affiliateCode": "1d47bbe5-c7d3-4580-ad2a-c4b192eeeb47"
+ }
+ ```
+
+- **Errors**:
+
+ - **400 Bad Request**: Invalid request body.
+
+ - **500 Internal Server Error**: Failed to create the entity.
+
+- **Example Request with cURL**:
+
+```curl
+curl -X POST localhost:8000/users/ \
+-H "Content-Type: application/json" \
+-d '{
+ "name": "john",
+ "email": "john@gmail.com",
+ "phone": "+5519912345678"
+}'
+```
+
+
+###
+
+`/competitions/`
+
+
+POST /competitions/
+
+- **Description**: Creates a new competition.
+
+- **Headers**:
+
+ ```plaintext
+ Content-Type: application/json
+
+ ```
+
+- **Request Body**:
+
+ ```json
+ ```
+
+- **Success Response (201 Created)**:
+
+ ```json
+ {
+ "id": "06ae5f86-46dd-42d3-8e6d-2abe26f6b07e"
+ }
+ ```
+
+- **Errors**:
+
+ - **409 Conflict**: competition already activated.
+
+ - **500 Internal Server Error**: error create competition.
+
+- **Example Request with cURL**:
+
+```curl
+curl -X POST localhost:8000/competitions/ \
+-H "Content-Type: application/json" \
+```
+
+
+
+GET /competitions/
+
+- **Description**: Get competition activated.
+
+- **Headers**:
+
+ ```plaintext
+ Content-Type: application/json
+
+ ```
+
+- **Success Response (200 OK / 204 No Content)**:
+
+ ```json
+ {
+ "id": "9fbae8ae-3ba9-4582-a931-04d2e3c6aa93",
+ "createdAt": 1743096181,
+ "status": true
+ }
+ ```
+
+- **Example Request with cURL**:
+
+```curl
+curl -X GET localhost:8000/competitions/ \
+-H "Content-Type: application/json" \
+```
+
+
+
+PUT /competitions/
+
+- **Description**: Close Competition.
+
+- **Headers**:
+
+ ```plaintext
+ Content-Type: application/json
+
+ ```
+
+- **Query Parameters**:
+
+ **ID** (required, UUID): Competiton ID.
+
+- **Request Body**:
+
+ ```json
+ ```
+
+- **Success Response (204 No Content)**:
+
+ ```json
+ ```
+
+- **Errors**:
+
+ - **400 Bad Request**: invalid id.
+
+ - **404 Not Found**: competition not found.
+
+ - **409 Conflict**: competition already activated.
+
+ - **500 Internal Server Error**: closed competition error.
+
+- **Example Request with cURL**:
+
+```curl
+curl -X PUT localhost:8000/competitions/?ID=e4b5c0cc-2f47-4b29-9883-84f314600f71\
+-H "Content-Type: application/json" \
+```
+
+
+
+GET /competitions/reports/
+
+- **Description**: Get competition reports.
+
+- **Headers**:
+
+ ```plaintext
+ Content-Type: application/json
+ ```
+
+- **Query Parameters**:
+
+ **ID** (required, UUID): Competiton ID.
+
+- **Success Response (200 OK)**:
+
+ ```json
+ [
+ {
+ "name": "User 6",
+ "points": 10
+ },
+ {
+ "name": "User 5",
+ "points": 10
+ },
+ {
+ "name": "User 13",
+ "points": 9
+ },
+ {
+ "name": "User 8",
+ "points": 8
+ },
+ {
+ "name": "User 9",
+ "points": 8
+ },
+ {
+ "name": "User 7",
+ "points": 6
+ },
+ {
+ "name": "User 2",
+ "points": 4
+ },
+ {
+ "name": "User 11",
+ "points": 4
+ },
+ {
+ "name": "User 15",
+ "points": 4
+ },
+ {
+ "name": "User 1",
+ "points": 2
+ }
+ ]
+ ```
+
+- **Example Request with cURL**:
+
+```curl
+curl -X GET localhost:8000/competitions/reports/?ID=d3dd9c62-5cc0-4b57-b341-ea1876dadac6 \
+-H "Content-Type: application/json" \
+```
+
+
+## 🗄️ Database Diagram
+
+
+
+## 📦 Usage libraries:
+
+- [gin](github.com/gin-gonic/gin)
+
+- [gorm](https://gorm.io/)
+
+- [gorm/postgres](https://github.com/go-gorm/postgres)
+
+- [uuid](github.com/google/uuid)
+
+- [gomail](https://pkg.go.dev/gopkg.in/gomail.v2?utm_source=godoc)
\ No newline at end of file
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 0000000..0ad035c
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,50 @@
+module github.com/joaooliveira247/backend-test
+
+go 1.24.1
+
+require (
+ github.com/gin-gonic/gin v1.10.0
+ github.com/google/uuid v1.6.0
+ gorm.io/driver/postgres v1.5.11
+ gorm.io/gorm v1.25.12
+)
+
+require (
+ 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/gabriel-vasile/mimetype v1.4.8 // indirect
+ github.com/gin-contrib/sse v1.0.0 // 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.25.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/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/leodido/go-urn v1.4.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/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.15.0 // indirect
+ golang.org/x/crypto v0.36.0 // indirect
+ golang.org/x/net v0.37.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/time v0.11.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644
index 0000000..d638a93
--- /dev/null
+++ b/backend/go.sum
@@ -0,0 +1,120 @@
+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/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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/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/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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+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.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
+github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
+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.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+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/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/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+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=
+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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+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.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/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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/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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+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=
diff --git a/backend/main.go b/backend/main.go
new file mode 100644
index 0000000..35092b5
--- /dev/null
+++ b/backend/main.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+ "log"
+
+ "github.com/gin-gonic/gin"
+ "github.com/joaooliveira247/backend-test/src/config"
+ "github.com/joaooliveira247/backend-test/src/db"
+ "github.com/joaooliveira247/backend-test/src/middlewares"
+ "github.com/joaooliveira247/backend-test/src/routes"
+)
+
+func init() {
+ config.LoadEnv()
+}
+
+func main() {
+
+ gormDB, err := db.GetDBConnection()
+
+ if err != nil {
+ log.Fatalf("DATABASE Connection: %v", err)
+ }
+
+ if err := db.CreateTables(gormDB); err != nil {
+ log.Fatalf("DATABASE Tables Creation: %v", err)
+ return
+ }
+
+ api := gin.Default()
+ api.Use(middlewares.RateLimiter)
+ routes.RegistryRoutes(api)
+
+ if err := api.Run(":8000"); err != nil {
+ log.Fatalf("API RUN: err")
+ }
+}
diff --git a/backend/src/config/config.go b/backend/src/config/config.go
new file mode 100644
index 0000000..9176f3b
--- /dev/null
+++ b/backend/src/config/config.go
@@ -0,0 +1,15 @@
+package config
+
+import "os"
+
+var (
+ DATABASE_URL = ""
+ SERVICE_EMAIL = ""
+ PASSWORD_SERVICE_EMAIL = ""
+)
+
+func LoadEnv() {
+ DATABASE_URL = os.Getenv("DATABASE_URL")
+ SERVICE_EMAIL = os.Getenv("SERVICE_EMAIL")
+ PASSWORD_SERVICE_EMAIL = os.Getenv("PASSWORD_SERVICE_EMAIL")
+}
diff --git a/backend/src/controllers/competitions.go b/backend/src/controllers/competitions.go
new file mode 100644
index 0000000..896def3
--- /dev/null
+++ b/backend/src/controllers/competitions.go
@@ -0,0 +1,173 @@
+package controllers
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/joaooliveira247/backend-test/src/models"
+ "github.com/joaooliveira247/backend-test/src/repositories"
+ "github.com/joaooliveira247/backend-test/src/services"
+ "gorm.io/gorm"
+)
+
+type CompetitionsController struct {
+ UserRepository repositories.UsersRepository
+ CompetitionRepository repositories.CompetitionsRepository
+ PointRepository repositories.PointsRepository
+}
+
+func NewCompetitionsController(
+ userRepo repositories.UsersRepository,
+ compRepo repositories.CompetitionsRepository,
+ pointRepo repositories.PointsRepository,
+) *CompetitionsController {
+ return &CompetitionsController{userRepo, compRepo, pointRepo}
+}
+
+func (ctrl *CompetitionsController) Create(ctx *gin.Context) {
+ compCheck, _ := ctrl.CompetitionRepository.GetActiveCompetition()
+
+ if compCheck.IsEmpty() {
+ id, err := ctrl.CompetitionRepository.Create(
+ &models.Competitions{Status: true},
+ )
+
+ if err != nil {
+ ctx.JSON(
+ http.StatusInternalServerError,
+ gin.H{"error": "create competition", "details": err.Error()},
+ )
+ return
+ }
+ ctx.JSON(http.StatusCreated, gin.H{"id": id})
+ return
+ }
+
+ ctx.JSON(
+ http.StatusConflict,
+ gin.H{
+ "error": "competition already activated",
+ "details": compCheck.ID,
+ },
+ )
+}
+
+func (ctrl *CompetitionsController) GetCompetition(ctx *gin.Context) {
+ // main request for main screen
+ compCheck, _ := ctrl.CompetitionRepository.GetActiveCompetition()
+
+ if !compCheck.IsEmpty() {
+ if code := ctx.Query("affiliateCode"); code != "" {
+ user, _ := ctrl.UserRepository.GetUserByAffiliateCode(code)
+
+ if !user.IsEmpty() {
+ ctrl.PointRepository.AddPoint(
+ &models.Points{
+ UserID: user.ID,
+ CompetitionID: compCheck.ID,
+ },
+ )
+ services.SendEmail(
+ user.Email,
+ "Carbon Offset Competition",
+ fmt.Sprintf(
+ "+1 point using your affiliate code: %s",
+ code,
+ ),
+ )
+ }
+ }
+ ctx.JSON(http.StatusOK, compCheck)
+ return
+ }
+ ctx.JSON(http.StatusNoContent, nil)
+}
+
+func (ctrl *CompetitionsController) CloseCompetition(ctx *gin.Context) {
+ compID, err := uuid.Parse(ctx.Query("ID"))
+
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "error": "invalid id format", "details": err.Error(),
+ })
+ return
+ }
+
+ if err := ctrl.CompetitionRepository.CloseCompetition(compID); err != nil {
+
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ ctx.JSON(
+ http.StatusNotFound,
+ gin.H{
+ "error": "competition not found",
+ "details": err.Error(),
+ },
+ )
+ return
+ }
+
+ ctx.JSON(
+ http.StatusInternalServerError,
+ gin.H{"error": "closed competition error", "details": err.Error()},
+ )
+ return
+ }
+
+ winners, err := ctrl.CompetitionRepository.GetCompetitionReport(compID)
+
+ if err != nil {
+ log.Printf("error pick winners: %v", err)
+ }
+
+ for i, report := range winners {
+ services.SendEmail(
+ report.Email,
+ "Carbon Offset Competition Ended",
+ fmt.Sprintf(
+ "Congratulations Carbon Offset Competition: %s ended, your position was %d",
+ compID,
+ i + 1,
+ ),
+ )
+ }
+
+ ctx.JSON(http.StatusNoContent, nil)
+}
+
+func (ctrl *CompetitionsController) GetCompetitionReport(ctx *gin.Context) {
+ compID, err := uuid.Parse(ctx.Query("ID"))
+
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "error": "invalid id format", "details": err.Error(),
+ })
+ return
+ }
+
+ comp, _ := ctrl.CompetitionRepository.GetCompetitionByID(compID)
+
+ if comp.Status {
+ ctx.JSON(http.StatusConflict, gin.H{"error": "competition not closed"})
+ return
+ }
+
+ reports, err := ctrl.CompetitionRepository.GetCompetitionReport(compID)
+
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{
+ "error": "gen competition report", "details": err.Error(),
+ })
+ return
+ }
+
+ if reports == nil {
+ ctx.JSON(http.StatusNotFound, gin.H{"error": "competiton not found"})
+ return
+ }
+
+ ctx.JSON(http.StatusOK, reports)
+}
diff --git a/backend/src/controllers/users.go b/backend/src/controllers/users.go
new file mode 100644
index 0000000..be4ef9f
--- /dev/null
+++ b/backend/src/controllers/users.go
@@ -0,0 +1,79 @@
+package controllers
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/joaooliveira247/backend-test/src/models"
+ "github.com/joaooliveira247/backend-test/src/repositories"
+ "github.com/joaooliveira247/backend-test/src/utils"
+)
+
+type UsersController struct {
+ userRepository repositories.UsersRepository
+ competitionRepository repositories.CompetitionsRepository
+ pointRepository repositories.PointsRepository
+}
+
+func NewUsersController(
+ userRepo repositories.UsersRepository,
+ compRepo repositories.CompetitionsRepository,
+ pointRepo repositories.PointsRepository,
+) *UsersController {
+ return &UsersController{userRepo, compRepo, pointRepo}
+}
+
+func (ctrl *UsersController) CreateUser(ctx *gin.Context) {
+ var user models.Users
+
+ if err := ctx.ShouldBindJSON(&user); err != nil {
+ ctx.JSON(
+ http.StatusBadRequest,
+ gin.H{"error": "invalid request body", "details": err.Error()},
+ )
+ return
+ }
+
+ if err := user.Validate(); err != nil {
+ ctx.JSON(
+ http.StatusBadRequest,
+ gin.H{"error": "invalid request body", "details": err.Error()},
+ )
+ return
+ }
+
+ code, err := utils.GenerateAffiliateCode()
+
+ if err != nil {
+ ctx.JSON(
+ http.StatusInternalServerError,
+ gin.H{
+ "error": "error when try create affiliate code",
+ "details": err.Error(),
+ },
+ )
+ return
+ }
+
+ user.AffiliateCode = code
+
+ affiliateCode, err := ctrl.userRepository.Create(&user)
+
+ if err != nil {
+ ctx.JSON(
+ http.StatusInternalServerError,
+ gin.H{"error": "database error", "details": err.Error()},
+ )
+ return
+ }
+
+ comp, _ := ctrl.competitionRepository.GetActiveCompetition()
+
+ if !comp.IsEmpty() {
+ ctrl.pointRepository.AddPoint(
+ &models.Points{UserID: user.ID, CompetitionID: comp.ID},
+ )
+ }
+
+ ctx.JSON(http.StatusCreated, gin.H{"affiliateCode": affiliateCode})
+}
\ No newline at end of file
diff --git a/backend/src/db/db.go b/backend/src/db/db.go
new file mode 100644
index 0000000..c54f720
--- /dev/null
+++ b/backend/src/db/db.go
@@ -0,0 +1,28 @@
+package db
+
+import (
+ "github.com/joaooliveira247/backend-test/src/config"
+ "github.com/joaooliveira247/backend-test/src/models"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+func GetDBConnection() (*gorm.DB, error) {
+ db, err := gorm.Open(
+ postgres.Open(config.DATABASE_URL),
+ &gorm.Config{Logger: logger.Default.LogMode(logger.Info)},
+ )
+
+ if err != nil {
+ return nil, err
+ }
+ return db, nil
+}
+
+func CreateTables(db *gorm.DB) error {
+ if err := db.AutoMigrate(&models.Users{}, &models.Competitions{}, &models.Points{}); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/backend/src/middlewares/limit.go b/backend/src/middlewares/limit.go
new file mode 100644
index 0000000..364e486
--- /dev/null
+++ b/backend/src/middlewares/limit.go
@@ -0,0 +1,37 @@
+package middlewares
+
+import (
+ "net/http"
+ "sync"
+
+ "github.com/gin-gonic/gin"
+ "golang.org/x/time/rate"
+)
+
+var limiters = sync.Map{}
+
+func getLimiter(ip string) *rate.Limiter {
+ limiter, ok := limiters.Load(ip)
+
+ if !ok {
+ newLimiter := rate.NewLimiter(1, 5)
+ limiters.Store(ip, newLimiter)
+ return newLimiter
+ }
+ return limiter.(*rate.Limiter)
+}
+
+func RateLimiter(ctx *gin.Context) {
+ ip := ctx.ClientIP()
+ limiter := getLimiter(ip)
+
+ if !limiter.Allow() {
+ ctx.JSON(
+ http.StatusTooManyRequests,
+ gin.H{"error": "too many requests"},
+ )
+ ctx.Abort()
+ return
+ }
+ ctx.Next()
+}
diff --git a/backend/src/models/competiotions.go b/backend/src/models/competiotions.go
new file mode 100644
index 0000000..6c061ef
--- /dev/null
+++ b/backend/src/models/competiotions.go
@@ -0,0 +1,19 @@
+package models
+
+import "github.com/google/uuid"
+
+type Competitions struct {
+ BaseModel
+ Status bool `json:"status" gorm:"type:boolean;not null;column:status"`
+}
+
+type CompetitionReport struct {
+ Name string `json:"name" gorm:"column:name"`
+ Points uint64 `json:"points" gorm:"column:points"`
+ Email string `json:"-" gorm:"column:email"`
+ Phone string `json:"-" gorm:"column:phone"`
+}
+
+func (comp *Competitions) IsEmpty() bool {
+ return comp.ID == uuid.Nil
+}
diff --git a/backend/src/models/models.go b/backend/src/models/models.go
new file mode 100644
index 0000000..8f7fc96
--- /dev/null
+++ b/backend/src/models/models.go
@@ -0,0 +1,8 @@
+package models
+
+import "github.com/google/uuid"
+
+type BaseModel struct {
+ ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
+ CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"`
+}
diff --git a/backend/src/models/points.go b/backend/src/models/points.go
new file mode 100644
index 0000000..fd7f0e9
--- /dev/null
+++ b/backend/src/models/points.go
@@ -0,0 +1,9 @@
+package models
+
+import "github.com/google/uuid"
+
+type Points struct {
+ BaseModel
+ CompetitionID uuid.UUID `gorm:"type:uuid;column:competition_id"`
+ UserID uuid.UUID `gorm:"type:uuid;column:user_id"`
+}
\ No newline at end of file
diff --git a/backend/src/models/users.go b/backend/src/models/users.go
new file mode 100644
index 0000000..f5120f3
--- /dev/null
+++ b/backend/src/models/users.go
@@ -0,0 +1,29 @@
+package models
+
+import (
+ "errors"
+
+ "github.com/google/uuid"
+ "github.com/joaooliveira247/backend-test/src/utils"
+)
+
+type Users struct {
+ BaseModel
+ Name string `json:"name" binding:"required,gt=1,lt=256" gorm:"type:varchar(255);not null;column:name"`
+ Email string `json:"email" binding:"required,gt=4,lt=256" gorm:"type:varchar(255);unique;not null;column:email"`
+ Phone string `json:"phone" binding:"required,gt=7,lt=16" gorm:"type:varchar(16);not null;column:phone"`
+ AffiliateCode string `json:"-" gorm:"type:varchar(8);unique;not null;column:affiliate_code"`
+}
+
+func (user *Users) Validate() error {
+ if !utils.EmailValidator(user.Email) {
+ return errors.New("invalid email format")
+ } else if !utils.IsPhoneNumber(user.Phone) {
+ return errors.New("invalid phone format")
+ }
+ return nil
+}
+
+func (user *Users) IsEmpty() bool {
+ return user.ID == uuid.Nil
+}
diff --git a/backend/src/repositories/competitions.go b/backend/src/repositories/competitions.go
new file mode 100644
index 0000000..476ca8d
--- /dev/null
+++ b/backend/src/repositories/competitions.go
@@ -0,0 +1,91 @@
+package repositories
+
+import (
+ "github.com/google/uuid"
+ "github.com/joaooliveira247/backend-test/src/models"
+ "gorm.io/gorm"
+)
+
+type CompetitionsRepository struct {
+ db *gorm.DB
+}
+
+func NewCompetiotionsRepository(db *gorm.DB) CompetitionsRepository {
+ return CompetitionsRepository{db}
+}
+
+func (repository *CompetitionsRepository) Create(
+ competiotion *models.Competitions,
+) (uuid.UUID, error) {
+ result := repository.db.Create(&competiotion)
+
+ if err := result.Error; err != nil {
+ return uuid.UUID{}, err
+ }
+
+ return competiotion.ID, nil
+}
+
+func (repository *CompetitionsRepository) GetActiveCompetition() (models.Competitions, error) {
+ var competition models.Competitions
+
+ if err := repository.db.First(&competition, "status = true").Error; err != nil {
+ return models.Competitions{}, err
+ }
+
+ return competition, nil
+}
+
+func (repository *CompetitionsRepository) GetCompetitionByID(competitionID uuid.UUID) (models.Competitions, error) {
+ var competition models.Competitions
+
+ if err := repository.db.First(&competition, "id = ?", competitionID).Error; err != nil {
+ return models.Competitions{}, err
+ }
+
+ return competition, nil
+}
+
+func (repository *CompetitionsRepository) CloseCompetition(
+ competitionID uuid.UUID,
+) error {
+ var competition models.Competitions
+
+ if err := repository.db.First(&competition, "id = ?", competitionID).Error; err != nil {
+ return err
+ }
+
+ competition.Status = false
+
+ if err := repository.db.Save(&competition).Error; err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (repository *CompetitionsRepository) GetCompetitionReport(
+ competitionID uuid.UUID,
+) ([]models.CompetitionReport, error) {
+ rawQuery := `
+SELECT
+ u.name,
+ u.email,
+ u.phone,
+ COUNT(*) AS points
+FROM points p
+INNER JOIN users u ON p.user_id = u.id
+WHERE p.competition_id = ?
+GROUP BY u.name, u.email, u.phone
+ORDER BY points DESC
+LIMIT 10;
+ `
+
+ var reports []models.CompetitionReport
+
+ if err := repository.db.Raw(rawQuery, competitionID).Scan(&reports).Error; err != nil {
+ return nil, err
+ }
+
+ return reports, nil
+}
diff --git a/backend/src/repositories/points.go b/backend/src/repositories/points.go
new file mode 100644
index 0000000..1269b2e
--- /dev/null
+++ b/backend/src/repositories/points.go
@@ -0,0 +1,22 @@
+package repositories
+
+import (
+ "github.com/joaooliveira247/backend-test/src/models"
+ "gorm.io/gorm"
+)
+
+type PointsRepository struct {
+ db *gorm.DB
+}
+
+func NewPointsRepository(db *gorm.DB) PointsRepository {
+ return PointsRepository{db}
+}
+
+func (repository *PointsRepository) AddPoint(point *models.Points) error {
+ if err := repository.db.Create(point).Error; err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/backend/src/repositories/users.go b/backend/src/repositories/users.go
new file mode 100644
index 0000000..abece43
--- /dev/null
+++ b/backend/src/repositories/users.go
@@ -0,0 +1,36 @@
+package repositories
+
+import (
+ "github.com/joaooliveira247/backend-test/src/models"
+ "gorm.io/gorm"
+)
+
+type UsersRepository struct {
+ db *gorm.DB
+}
+
+func NewUsersRepository(db *gorm.DB) UsersRepository {
+ return UsersRepository{db}
+}
+
+func (repository *UsersRepository) Create(user *models.Users) (string, error) {
+ result := repository.db.Create(&user)
+
+ if err := result.Error; err != nil {
+ return "", err
+ }
+
+ return user.AffiliateCode, nil
+}
+
+func (repository *UsersRepository) GetUserByAffiliateCode(
+ code string,
+) (models.Users, error) {
+ var user models.Users
+
+ if err := repository.db.First(&user, "affiliate_code = ?", code).Error; err != nil {
+ return models.Users{}, err
+ }
+
+ return user, nil
+}
diff --git a/backend/src/routes/competitions.go b/backend/src/routes/competitions.go
new file mode 100644
index 0000000..5166430
--- /dev/null
+++ b/backend/src/routes/competitions.go
@@ -0,0 +1,36 @@
+package routes
+
+import (
+ "log"
+
+ "github.com/gin-gonic/gin"
+ "github.com/joaooliveira247/backend-test/src/controllers"
+ "github.com/joaooliveira247/backend-test/src/db"
+ "github.com/joaooliveira247/backend-test/src/repositories"
+)
+
+func CompetitionsRoute(eng *gin.Engine) {
+ gormDB, err := db.GetDBConnection()
+
+ if err != nil {
+ log.Fatalf("Users Route: %v", err)
+ }
+
+ usersReposiotry := repositories.NewUsersRepository(gormDB)
+ compsRepository := repositories.NewCompetiotionsRepository(gormDB)
+ pointsRepository := repositories.NewPointsRepository(gormDB)
+
+ controller := controllers.NewCompetitionsController(
+ usersReposiotry,
+ compsRepository,
+ pointsRepository,
+ )
+
+ competitionsGroup := eng.Group("/competitions")
+ {
+ competitionsGroup.POST("/", controller.Create)
+ competitionsGroup.GET("/", controller.GetCompetition)
+ competitionsGroup.PUT("/", controller.CloseCompetition)
+ competitionsGroup.GET("/reports/", controller.GetCompetitionReport)
+ }
+}
\ No newline at end of file
diff --git a/backend/src/routes/routes.go b/backend/src/routes/routes.go
new file mode 100644
index 0000000..9049193
--- /dev/null
+++ b/backend/src/routes/routes.go
@@ -0,0 +1,8 @@
+package routes
+
+import "github.com/gin-gonic/gin"
+
+func RegistryRoutes(eng *gin.Engine) {
+ UsersRoute(eng)
+ CompetitionsRoute(eng)
+}
diff --git a/backend/src/routes/users.go b/backend/src/routes/users.go
new file mode 100644
index 0000000..0c11df8
--- /dev/null
+++ b/backend/src/routes/users.go
@@ -0,0 +1,33 @@
+package routes
+
+import (
+ "log"
+
+ "github.com/gin-gonic/gin"
+ "github.com/joaooliveira247/backend-test/src/controllers"
+ "github.com/joaooliveira247/backend-test/src/db"
+ "github.com/joaooliveira247/backend-test/src/repositories"
+)
+
+func UsersRoute(eng *gin.Engine) {
+ gormDB, err := db.GetDBConnection()
+
+ if err != nil {
+ log.Fatalf("Users Route: %v", err)
+ }
+
+ usersReposiotry := repositories.NewUsersRepository(gormDB)
+ compsRepository := repositories.NewCompetiotionsRepository(gormDB)
+ pointsRepository := repositories.NewPointsRepository(gormDB)
+
+ controller := controllers.NewUsersController(
+ usersReposiotry,
+ compsRepository,
+ pointsRepository,
+ )
+
+ usersGroup := eng.Group("/users")
+ {
+ usersGroup.POST("/", controller.CreateUser)
+ }
+}
diff --git a/backend/src/services/email.go b/backend/src/services/email.go
new file mode 100644
index 0000000..db38fa9
--- /dev/null
+++ b/backend/src/services/email.go
@@ -0,0 +1,42 @@
+package services
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/joaooliveira247/backend-test/src/config"
+ "gopkg.in/gomail.v2"
+)
+
+func SendEmail(to, subject, message string) {
+ smtpHost := "smtp.gmail.com"
+ smtpPort := 587
+
+ msg := gomail.NewMessage()
+
+ headers := map[string]string{
+ "From": config.SERVICE_EMAIL,
+ "To": to,
+ "Subject": subject,
+ }
+
+ for header, value := range headers {
+ msg.SetHeader(header, value)
+ }
+
+ msg.SetBody(
+ "text/html",
+ fmt.Sprintf("
Carbon Offset Competition
%s
", message),
+ )
+
+ server := gomail.NewDialer(
+ smtpHost,
+ smtpPort,
+ config.SERVICE_EMAIL,
+ config.PASSWORD_SERVICE_EMAIL,
+ )
+
+ if err := server.DialAndSend(msg); err != nil {
+ log.Fatalf("Send Email: %v", err)
+ }
+}
diff --git a/backend/src/utils/utils.go b/backend/src/utils/utils.go
new file mode 100644
index 0000000..e938522
--- /dev/null
+++ b/backend/src/utils/utils.go
@@ -0,0 +1,21 @@
+package utils
+
+import (
+ "crypto/rand"
+ "math/big"
+)
+
+const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+
+func GenerateAffiliateCode() (string, error) {
+ code := make([]byte, 8)
+ for i := range code {
+ n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
+ if err != nil {
+ return "", err
+ }
+ code[i] = charset[n.Int64()]
+ }
+
+ return string(code), nil
+}
diff --git a/backend/src/utils/validators.go b/backend/src/utils/validators.go
new file mode 100644
index 0000000..d90cb5d
--- /dev/null
+++ b/backend/src/utils/validators.go
@@ -0,0 +1,28 @@
+package utils
+
+import (
+ "net"
+ "regexp"
+ "strings"
+)
+
+func EmailValidator(email string) bool {
+ regex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
+ re := regexp.MustCompile(regex)
+ if !re.MatchString(email) {
+ return false
+ }
+
+ parts := strings.Split(email, "@")
+ if len(parts) != 2 {
+ return false
+ }
+ mxRecords, err := net.LookupMX(parts[1])
+ return err == nil && len(mxRecords) > 0
+}
+
+func IsPhoneNumber(phone string) bool {
+ re := regexp.MustCompile(`^\+55\d{11}$`)
+
+ return re.MatchString(phone)
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..c010ff6
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,32 @@
+version: "3.1"
+
+services:
+ carbon_offset_api:
+ build:
+ context: ./backend
+ dockerfile: Dockerfile
+ environment:
+ - DATABASE_URL= host=carbon_offset_db user=user password=passwd dbname=carbon_offset port=5432 sslmode=disable
+ - SERVICE_EMAIL=${SERVICE_EMAIL}
+ - PASSWORD_SERVICE_EMAIL=${PASSWORD_SERVICE_EMAIL}
+ networks:
+ - carbon_offset_network
+ ports:
+ - 8000:8000
+ volumes:
+ - ./backend:/app
+
+ carbon_offset_db:
+ image: postgres:alpine3.20
+ restart: "no"
+ environment:
+ POSTGRES_USER: user
+ POSTGRES_PASSWORD: passwd
+ POSTGRES_DB: carbon_offset
+ networks:
+ - carbon_offset_network
+ ports:
+ - 5432:5432
+
+networks:
+ carbon_offset_network:
\ No newline at end of file