├── .dockerignore ├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _scripts ├── mongodb │ └── init.js └── postgres │ └── init.sql ├── adapter ├── api │ ├── action │ │ ├── create_account.go │ │ ├── create_account_test.go │ │ ├── create_transfer.go │ │ ├── create_transfer_test.go │ │ ├── find_account_balance.go │ │ ├── find_account_balance_test.go │ │ ├── find_all_account.go │ │ ├── find_all_account_test.go │ │ ├── find_all_transfer.go │ │ ├── find_all_transfer_test.go │ │ ├── health_check.go │ │ └── health_check_test.go │ ├── logging │ │ ├── error.go │ │ └── info.go │ ├── middleware │ │ └── logger.go │ └── response │ │ ├── error.go │ │ └── success.go ├── logger │ └── logger.go ├── presenter │ ├── create_account.go │ ├── create_account_test.go │ ├── create_transfer.go │ ├── create_transfer_test.go │ ├── find_account_balance.go │ ├── find_account_balance_test.go │ ├── find_all_account.go │ ├── find_all_account_test.go │ ├── find_all_transfer.go │ └── find_all_transfer_test.go ├── repository │ ├── account_mongodb.go │ ├── account_postgres.go │ ├── nosql.go │ ├── sql.go │ ├── transfer_mongodb.go │ └── transfer_postgres.go └── validator │ └── validator.go ├── clean.png ├── create_account.png ├── docker-compose.yml ├── domain ├── account.go ├── account_test.go ├── money.go ├── transfer.go └── uuid.go ├── go.mod ├── go.sum ├── infrastructure ├── database │ ├── config.go │ ├── factory_nosql.go │ ├── factory_sql.go │ ├── mongo_handler.go │ ├── mongo_handler_deprecated.go │ └── postgres_handler.go ├── http_server.go ├── log │ ├── factory.go │ ├── logger_mock.go │ ├── logrus.go │ └── zap.go ├── router │ ├── factory.go │ ├── gin.go │ └── gorilla_mux.go └── validation │ ├── factory.go │ └── go_playground.go ├── main.go └── usecase ├── create_account.go ├── create_account_test.go ├── create_transfer.go ├── create_transfer_test.go ├── find_account_balance.go ├── find_account_balance_test.go ├── find_all_account.go ├── find_all_account_test.go ├── find_all_transfer.go └── find_all_transfer_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | */Makefile 2 | */README.md 3 | */.git* 4 | */coverage* 5 | */.idea* 6 | */docker-compose.yml 7 | */Dockerfile* 8 | */.env* 9 | *.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=go-bank-transfer 2 | APP_PORT=3001 3 | 4 | MONGODB_HOST=mongodb 5 | MONGODB_DATABASE=bank 6 | MONGODB_ROOT_USER=root 7 | MONGODB_ROOT_PASSWORD=password123 8 | 9 | POSTGRES_HOST=postgres 10 | POSTGRES_PORT=5432 11 | POSTGRES_USER=dev 12 | POSTGRES_PASSWORD=dev 13 | POSTGRES_DATABASE=bank 14 | POSTGRES_DRIVER=postgres 15 | 16 | GO111MODULE=on 17 | CGO_ENABLED=0 18 | GOOS=linux 19 | GOARCH=amd64 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.21.x] 8 | platform: [ubuntu-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Creating .env 18 | uses: canastro/copy-action@0.0.2 19 | with: 20 | source: ".env.example" 21 | target: ".env" 22 | - name: Run tests 23 | run: go test -v -covermode=count ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage* 2 | .idea/ 3 | .env 4 | *tmp* 5 | main -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.21.x 5 | 6 | script: 7 | - make test-local 8 | - make test-report-text 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 AS base 2 | WORKDIR /app 3 | COPY . . 4 | 5 | FROM base AS debugger 6 | WORKDIR /app 7 | COPY . . 8 | RUN go get github.com/go-delve/delve/cmd/dlv 9 | EXPOSE 3001 40000 10 | ENTRYPOINT ["dlv", "debug", "--listen=:40000", "--headless", "--accept-multiclient", "--continue", "--api-version=2"] 11 | 12 | FROM base AS development 13 | WORKDIR /app 14 | COPY . . 15 | RUN go install github.com/cosmtrek/air@latest 16 | EXPOSE 3001 17 | ENTRYPOINT ["air"] 18 | 19 | FROM base AS builder 20 | WORKDIR /app 21 | COPY . . 22 | RUN go build -a --installsuffix cgo --ldflags="-s" -o main 23 | 24 | FROM alpine:latest AS production 25 | RUN apk --no-cache add ca-certificates 26 | COPY --from=builder /app . 27 | EXPOSE 3001 28 | ENTRYPOINT ["./main"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gabriel S. Facina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=development 2 | SYSTEM=go-clean-architecture 3 | SYSTEM_VERSION=$(shell git branch --show-current | cut -d '/' -f2) 4 | PWD=$(shell pwd -L) 5 | DOCKER_RUN=docker run --rm -it -w /app -v ${PWD}:/app -v ${GOPATH}/pkg/mod/cache:/go/pkg/mod/cache golang:1.21 6 | 7 | .PHONY: all 8 | all: help 9 | help: ## Display help screen 10 | @echo "Usage:" 11 | @echo " make [COMMAND]" 12 | @echo " make help \n" 13 | @echo "Commands: \n" 14 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 15 | 16 | .PHONY: test 17 | init: ## Create environment variables 18 | cp .env.example .env 19 | 20 | .PHONY: test-local 21 | test-local: ## Run local golang tests 22 | go test -cover -race ./... 23 | 24 | .PHONY: test 25 | test: ## Run golang tests 26 | ${DOCKER_RUN} go test -cover -race ./... 27 | 28 | .PHONY: test-report 29 | test-report: ## Run tests with HTML coverage report 30 | ${DOCKER_RUN} go test -covermode=count -coverprofile coverage.out ./... && \ 31 | go tool cover -html=coverage.out -o coverage.html && \ 32 | xdg-open ./coverage.html 33 | 34 | .PHONY: test-report-func 35 | test-report-func: ## Run tests with func report -covermode=set 36 | ${DOCKER_RUN} go test -covermode=set -coverprofile=coverage.out ./... && \ 37 | go tool cover -func=coverage.out 38 | 39 | .PHONY: test-report-text 40 | test-report-text: 41 | go test ./... -coverprofile=coverage.txt -covermode=atomic 42 | 43 | # https://golangci-lint.run/usage/linters/ 44 | .PHONY: lint 45 | lint: ## Lint with golangci-lint 46 | docker run --rm -it -v $(PWD):/app -w /app golangci/golangci-lint:v1.39-alpine \ 47 | golangci-lint run \ 48 | --exclude-use-default=false \ 49 | --enable=gocyclo \ 50 | --enable=bodyclose \ 51 | --enable=goconst \ 52 | --enable=sqlclosecheck \ 53 | --enable=rowserrcheck \ 54 | --enable=prealloc 55 | 56 | .PHONY: fmt 57 | fmt: ## Run go fmt 58 | go fmt ./... 59 | 60 | .PHONY: vet 61 | vet: ## Run go vet 62 | go vet ./... 63 | 64 | .PHONY: up 65 | up: ## Run docker-compose up for creating and starting containers 66 | docker-compose up -d 67 | 68 | .PHONY: down 69 | down: ## Run docker-compose down for stopping and removing containers, networks, images, and volumes 70 | docker-compose down --remove-orphans 71 | 72 | .PHONY: logs 73 | logs: ## View container log 74 | docker-compose logs -f app 75 | 76 | .PHONY: clean 77 | clean: ## Clean build bin/ 78 | @rm -rf bin/ 79 | 80 | .PHONY: build 81 | build: clean ## Build golang project 82 | go build -o bin/$(SYSTEM) main.go 83 | 84 | .PHONY: run 85 | run: ## Run golang project 86 | go run main.go 87 | 88 | .PHONY: docker-clean 89 | docker-clean: ## Clean docker removes image 90 | docker rmi gsabadini/$(SYSTEM):$(SYSTEM_VERSION) 91 | 92 | .PHONY: docker-build 93 | docker-build: ## Build docker image for the project 94 | @docker build --target production -t gsabadini/$(SYSTEM):$(SYSTEM_VERSION) . 95 | 96 | .PHONY: docker-run 97 | docker-run: docker-deps ## Run docker container for image project 98 | docker run --rm -it \ 99 | -e ENVIRONMENT=$(ENVIRONMENT) \ 100 | -e SYSTEM=$(SYSTEM) \ 101 | -e SYSTEM_VERSION=$(SYSTEM_VERSION) \ 102 | -p 3001:3001 \ 103 | --env-file .env \ 104 | --network go-clean-architecture_main \ 105 | --name $(SYSTEM)-$(SYSTEM_VERSION) gsabadini/$(SYSTEM):$(SYSTEM_VERSION) 106 | 107 | docker-deps: 108 | docker-compose up -d postgres mongodb-primary mongodb-secondary mongodb-arbiter 109 | sleep 3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to Go Clean Architecture

2 |

3 | Version 4 | 5 | Build 6 | 7 | 8 | License: MIT 9 | 10 | 11 | Build 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 | 21 | - The Go Clean Architecture is a user-friendly solution designed for a range of banking tasks, including account creation, account listing, checking the balance of specific accounts, facilitating transfers between accounts, and compiling transfer records. 22 | 23 | ## Architecture 24 | - This represents an endeavor to implement a clean architecture. In the event that you're not yet familiar with it, I'd like to provide you with a [reference](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). 25 | 26 | ![Clean Architecture](clean.png) 27 | 28 | ## Example create account use case 29 | 30 | ![Clean Architecture](create_account.png) 31 | 32 | ## Requirements/dependencies 33 | - Docker 34 | - Docker-compose 35 | 36 | ## Getting Started 37 | 38 | - Environment variables 39 | 40 | ```sh 41 | make init 42 | ``` 43 | 44 | - Starting API in development mode 45 | 46 | ```sh 47 | make up 48 | ``` 49 | 50 | - Run tests in container 51 | 52 | ```sh 53 | make test 54 | ``` 55 | 56 | - Run tests local (it is necessary to have golang installed) 57 | 58 | ```sh 59 | make test-local 60 | ``` 61 | 62 | - Run coverage report 63 | 64 | ```sh 65 | make test-report 66 | ``` 67 | ```sh 68 | make test-report-func 69 | ``` 70 | 71 | - View logs 72 | 73 | ```sh 74 | make logs 75 | ``` 76 | 77 | ## API Request 78 | 79 | | Endpoint | HTTP Method | Description | 80 | | --------------- | :---------------------: | :-----------------: | 81 | | `/v1/accounts` | `POST` | `Create accounts` | 82 | | `/v1/accounts` | `GET` | `List accounts` | 83 | | `/v1/accounts/{{account_id}}/balance` | `GET` | `Find balance account` | 84 | | `/v1/transfers`| `POST` | `Create transfer` | 85 | | `/v1/transfers`| `GET` | `List transfers` | 86 | | `/v1/health`| `GET` | `Health check` | 87 | 88 | ## Test endpoints API using curl 89 | 90 | - #### Creating new account 91 | 92 | `Request` 93 | ```bash 94 | curl -i --request POST 'http://localhost:3001/v1/accounts' \ 95 | --header 'Content-Type: application/json' \ 96 | --data-raw '{ 97 | "name": "Test", 98 | "cpf": "070.910.584-24", 99 | "balance": 100 100 | }' 101 | ``` 102 | 103 | `Response` 104 | ```json 105 | { 106 | "id":"5cf59c6c-0047-4b13-a118-65878313e329", 107 | "name":"Test", 108 | "cpf":"070.910.584-24", 109 | "balance":1, 110 | "created_at":"2020-11-02T14:50:46Z" 111 | } 112 | ``` 113 | - #### Listing accounts 114 | 115 | `Request` 116 | ```bash 117 | curl -i --request GET 'http://localhost:3001/v1/accounts' 118 | ``` 119 | 120 | `Response` 121 | ```json 122 | [ 123 | { 124 | "id": "5cf59c6c-0047-4b13-a118-65878313e329", 125 | "name": "Test", 126 | "cpf": "070.910.584-24", 127 | "balance": 1, 128 | "created_at": "2020-11-02T14:50:46Z" 129 | } 130 | ] 131 | ``` 132 | 133 | - #### Fetching account balance 134 | 135 | `Request` 136 | ```bash 137 | curl -i --request GET 'http://localhost:3001/v1/accounts/{{account_id}}/balance' 138 | ``` 139 | 140 | `Response` 141 | ```json 142 | { 143 | "balance": 1 144 | } 145 | ``` 146 | 147 | - #### Creating new transfer 148 | 149 | `Request` 150 | ```bash 151 | curl -i --request POST 'http://localhost:3001/v1/transfers' \ 152 | --header 'Content-Type: application/json' \ 153 | --data-raw '{ 154 | "account_origin_id": "{{account_id}}", 155 | "account_destination_id": "{{account_id}}", 156 | "amount": 100 157 | }' 158 | ``` 159 | 160 | `Response` 161 | ```json 162 | { 163 | "id": "b51cd6c7-a55c-491e-9140-91903fe66fa9", 164 | "account_origin_id": "{{account_id}}", 165 | "account_destination_id": "{{account_id}}", 166 | "amount": 1, 167 | "created_at": "2020-11-02T14:57:35Z" 168 | } 169 | ``` 170 | 171 | - #### Listing transfers 172 | 173 | `Request` 174 | ```bash 175 | curl -i --request GET 'http://localhost:3001/v1/transfers' 176 | ``` 177 | 178 | `Response` 179 | ```json 180 | [ 181 | { 182 | "id": "b51cd6c7-a55c-491e-9140-91903fe66fa9", 183 | "account_origin_id": "{{account_id}}", 184 | "account_destination_id": "{{account_id}}", 185 | "amount": 1, 186 | "created_at": "2020-11-02T14:57:35Z" 187 | } 188 | ] 189 | ``` 190 | 191 | ## Git workflow 192 | - Gitflow 193 | 194 | ## Code status 195 | - Development 196 | 197 | ## Author 198 | - Gabriel Sabadini Facina - [GSabadini](https://github.com/GSabadini) 199 | 200 | ## License 201 | Copyright © 2020 [GSabadini](https://github.com/GSabadini). 202 | This project is [MIT](LICENSE) licensed. 203 | -------------------------------------------------------------------------------- /_scripts/mongodb/init.js: -------------------------------------------------------------------------------- 1 | db = db.getSiblingDB('bank'); 2 | 3 | db.createUser({ 4 | user: 'dev', 5 | pwd: 'dev', 6 | roles: [ 7 | { 8 | role: 'root', 9 | db: 'admin', 10 | }, 11 | ], 12 | }); 13 | 14 | accounts = db.createCollection('accounts'); 15 | db.accounts.createIndex( { "cpf": 1 }, { unique: true } ) 16 | 17 | db.createCollection('transfers'); 18 | -------------------------------------------------------------------------------- /_scripts/postgres/init.sql: -------------------------------------------------------------------------------- 1 | GRANT ALL PRIVILEGES ON DATABASE bank TO dev; 2 | 3 | CREATE TABLE transfers ( 4 | id VARCHAR(36) PRIMARY KEY NOT NULL, 5 | account_origin_id VARCHAR NOT NULL, 6 | account_destination_id VARCHAR NOT NULL, 7 | amount BIGINT NOT NULL, 8 | created_at TIMESTAMP NOT NULL 9 | ); 10 | 11 | CREATE TABLE accounts ( 12 | id VARCHAR(36) PRIMARY KEY NOT NULL, 13 | name VARCHAR NOT NULL, 14 | cpf VARCHAR UNIQUE NOT NULL, 15 | balance BIGINT NOT NULL, 16 | created_at TIMESTAMP NOT NULL 17 | ); -------------------------------------------------------------------------------- /adapter/api/action/create_account.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gsabadini/go-clean-architecture/adapter/api/logging" 9 | "github.com/gsabadini/go-clean-architecture/adapter/api/response" 10 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 11 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 12 | "github.com/gsabadini/go-clean-architecture/usecase" 13 | ) 14 | 15 | type CreateAccountAction struct { 16 | uc usecase.CreateAccountUseCase 17 | log logger.Logger 18 | validator validator.Validator 19 | } 20 | 21 | func NewCreateAccountAction(uc usecase.CreateAccountUseCase, log logger.Logger, v validator.Validator) CreateAccountAction { 22 | return CreateAccountAction{ 23 | uc: uc, 24 | log: log, 25 | validator: v, 26 | } 27 | } 28 | 29 | func (a CreateAccountAction) Execute(w http.ResponseWriter, r *http.Request) { 30 | const logKey = "create_account" 31 | 32 | var input usecase.CreateAccountInput 33 | if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 34 | logging.NewError( 35 | a.log, 36 | err, 37 | logKey, 38 | http.StatusBadRequest, 39 | ).Log("error when decoding json") 40 | 41 | response.NewError(err, http.StatusBadRequest).Send(w) 42 | return 43 | } 44 | defer r.Body.Close() 45 | 46 | if errs := a.validateInput(input); len(errs) > 0 { 47 | logging.NewError( 48 | a.log, 49 | response.ErrInvalidInput, 50 | logKey, 51 | http.StatusBadRequest, 52 | ).Log("invalid input") 53 | 54 | response.NewErrorMessage(errs, http.StatusBadRequest).Send(w) 55 | return 56 | } 57 | 58 | a.cleanCPF(input.CPF) 59 | 60 | output, err := a.uc.Execute(r.Context(), input) 61 | if err != nil { 62 | logging.NewError( 63 | a.log, 64 | err, 65 | logKey, 66 | http.StatusInternalServerError, 67 | ).Log("error when creating a new account") 68 | 69 | response.NewError(err, http.StatusInternalServerError).Send(w) 70 | return 71 | } 72 | logging.NewInfo(a.log, logKey, http.StatusCreated).Log("success creating account") 73 | 74 | response.NewSuccess(output, http.StatusCreated).Send(w) 75 | } 76 | 77 | func (a CreateAccountAction) validateInput(input usecase.CreateAccountInput) []string { 78 | var msgs []string 79 | 80 | err := a.validator.Validate(input) 81 | if err != nil { 82 | for _, msg := range a.validator.Messages() { 83 | msgs = append(msgs, msg) 84 | } 85 | } 86 | 87 | return msgs 88 | } 89 | 90 | func (a CreateAccountAction) cleanCPF(cpf string) string { 91 | return strings.Replace(strings.Replace(cpf, ".", "", -1), "-", "", -1) 92 | } 93 | -------------------------------------------------------------------------------- /adapter/api/action/create_account_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 14 | "github.com/gsabadini/go-clean-architecture/infrastructure/validation" 15 | "github.com/gsabadini/go-clean-architecture/usecase" 16 | ) 17 | 18 | type mockAccountCreateAccount struct { 19 | result usecase.CreateAccountOutput 20 | err error 21 | } 22 | 23 | func (m mockAccountCreateAccount) Execute(_ context.Context, _ usecase.CreateAccountInput) (usecase.CreateAccountOutput, error) { 24 | return m.result, m.err 25 | } 26 | 27 | func TestCreateAccountAction_Execute(t *testing.T) { 28 | t.Parallel() 29 | 30 | validator, _ := validation.NewValidatorFactory(validation.InstanceGoPlayground) 31 | 32 | type args struct { 33 | rawPayload []byte 34 | } 35 | 36 | tests := []struct { 37 | name string 38 | args args 39 | ucMock usecase.CreateAccountUseCase 40 | expectedBody string 41 | expectedStatusCode int 42 | }{ 43 | { 44 | name: "CreateAccountAction success", 45 | args: args{ 46 | rawPayload: []byte( 47 | `{ 48 | "name": "test", 49 | "cpf": "44451598087", 50 | "balance": 10050 51 | }`, 52 | ), 53 | }, 54 | ucMock: mockAccountCreateAccount{ 55 | result: usecase.CreateAccountOutput{ 56 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 57 | Name: "Test", 58 | CPF: "07094564964", 59 | Balance: 10.5, 60 | CreatedAt: time.Time{}.String(), 61 | }, 62 | err: nil, 63 | }, 64 | expectedBody: `{"id":"3c096a40-ccba-4b58-93ed-57379ab04680","name":"Test","cpf":"07094564964","balance":10.5,"created_at":"0001-01-01 00:00:00 +0000 UTC"}`, 65 | expectedStatusCode: http.StatusCreated, 66 | }, 67 | { 68 | name: "CreateAccountAction success", 69 | args: args{ 70 | rawPayload: []byte( 71 | `{ 72 | "name": "test", 73 | "cpf": "44451598087", 74 | "balance": 100000 75 | }`, 76 | ), 77 | }, 78 | ucMock: mockAccountCreateAccount{ 79 | result: usecase.CreateAccountOutput{ 80 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 81 | Name: "Test", 82 | CPF: "07094564964", 83 | Balance: 10000, 84 | CreatedAt: time.Time{}.String(), 85 | }, 86 | err: nil, 87 | }, 88 | expectedBody: `{"id":"3c096a40-ccba-4b58-93ed-57379ab04680","name":"Test","cpf":"07094564964","balance":10000,"created_at":"0001-01-01 00:00:00 +0000 UTC"}`, 89 | expectedStatusCode: http.StatusCreated, 90 | }, 91 | { 92 | name: "CreateAccountAction generic error", 93 | args: args{ 94 | rawPayload: []byte( 95 | `{ 96 | "name": "test", 97 | "cpf": "44451598087", 98 | "balance": 10 99 | }`, 100 | ), 101 | }, 102 | ucMock: mockAccountCreateAccount{ 103 | result: usecase.CreateAccountOutput{}, 104 | err: errors.New("error"), 105 | }, 106 | expectedBody: `{"errors":["error"]}`, 107 | expectedStatusCode: http.StatusInternalServerError, 108 | }, 109 | { 110 | name: "CreateAccountAction error invalid balance", 111 | args: args{ 112 | rawPayload: []byte( 113 | `{ 114 | "name": "test", 115 | "cpf": "44451598087", 116 | "balance": -1 117 | }`, 118 | ), 119 | }, 120 | ucMock: mockAccountCreateAccount{ 121 | result: usecase.CreateAccountOutput{}, 122 | err: nil, 123 | }, 124 | expectedBody: `{"errors":["Balance must be greater than 0"]}`, 125 | expectedStatusCode: http.StatusBadRequest, 126 | }, 127 | { 128 | name: "CreateAccountAction error invalid fields", 129 | args: args{ 130 | rawPayload: []byte( 131 | `{ 132 | "name123": "test", 133 | "cpf1231": "44451598087", 134 | "balance12312": 1 135 | }`, 136 | ), 137 | }, 138 | ucMock: mockAccountCreateAccount{ 139 | result: usecase.CreateAccountOutput{}, 140 | err: nil, 141 | }, 142 | expectedBody: `{"errors":["Name is a required field","CPF is a required field","Balance must be greater than 0"]}`, 143 | expectedStatusCode: http.StatusBadRequest, 144 | }, 145 | { 146 | name: "CreateAccountAction error invalid JSON", 147 | args: args{ 148 | rawPayload: []byte( 149 | `{ 150 | "name": 151 | }`, 152 | ), 153 | }, 154 | ucMock: mockAccountCreateAccount{ 155 | result: usecase.CreateAccountOutput{}, 156 | err: nil, 157 | }, 158 | expectedBody: `{"errors":["invalid character '}' looking for beginning of value"]}`, 159 | expectedStatusCode: http.StatusBadRequest, 160 | }, 161 | } 162 | 163 | for _, tt := range tests { 164 | t.Run(tt.name, func(t *testing.T) { 165 | req, _ := http.NewRequest( 166 | http.MethodPost, 167 | "/accounts", 168 | bytes.NewReader(tt.args.rawPayload), 169 | ) 170 | 171 | var ( 172 | w = httptest.NewRecorder() 173 | action = NewCreateAccountAction(tt.ucMock, log.LoggerMock{}, validator) 174 | ) 175 | 176 | action.Execute(w, req) 177 | 178 | if w.Code != tt.expectedStatusCode { 179 | t.Errorf( 180 | "[TestCase '%s'] O handler retornou um HTTP status code inesperado: retornado '%v' esperado '%v'", 181 | tt.name, 182 | w.Code, 183 | tt.expectedStatusCode, 184 | ) 185 | } 186 | 187 | var result = strings.TrimSpace(w.Body.String()) 188 | if !strings.EqualFold(result, tt.expectedBody) { 189 | t.Errorf( 190 | "[TestCase '%s'] Result: '%v' | Expected: '%v'", 191 | tt.name, 192 | result, 193 | tt.expectedBody, 194 | ) 195 | } 196 | }) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /adapter/api/action/create_transfer.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/gsabadini/go-clean-architecture/adapter/api/logging" 9 | "github.com/gsabadini/go-clean-architecture/adapter/api/response" 10 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 11 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 12 | "github.com/gsabadini/go-clean-architecture/domain" 13 | "github.com/gsabadini/go-clean-architecture/usecase" 14 | ) 15 | 16 | type CreateTransferAction struct { 17 | log logger.Logger 18 | uc usecase.CreateTransferUseCase 19 | validator validator.Validator 20 | 21 | logKey, logMsg string 22 | } 23 | 24 | func NewCreateTransferAction(uc usecase.CreateTransferUseCase, log logger.Logger, v validator.Validator) CreateTransferAction { 25 | return CreateTransferAction{ 26 | uc: uc, 27 | log: log, 28 | validator: v, 29 | logKey: "create_transfer", 30 | logMsg: "creating a new transfer", 31 | } 32 | } 33 | 34 | func (t CreateTransferAction) Execute(w http.ResponseWriter, r *http.Request) { 35 | var input usecase.CreateTransferInput 36 | if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 37 | logging.NewError( 38 | t.log, 39 | err, 40 | t.logKey, 41 | http.StatusBadRequest, 42 | ).Log(t.logMsg) 43 | 44 | response.NewError(err, http.StatusBadRequest).Send(w) 45 | return 46 | } 47 | defer r.Body.Close() 48 | 49 | if errs := t.validateInput(input); len(errs) > 0 { 50 | logging.NewError( 51 | t.log, 52 | response.ErrInvalidInput, 53 | t.logKey, 54 | http.StatusBadRequest, 55 | ).Log(t.logMsg) 56 | 57 | response.NewErrorMessage(errs, http.StatusBadRequest).Send(w) 58 | return 59 | } 60 | 61 | output, err := t.uc.Execute(r.Context(), input) 62 | if err != nil { 63 | t.handleErr(w, err) 64 | return 65 | } 66 | 67 | logging.NewInfo(t.log, t.logKey, http.StatusCreated).Log(t.logMsg) 68 | 69 | response.NewSuccess(output, http.StatusCreated).Send(w) 70 | } 71 | 72 | func (t CreateTransferAction) handleErr(w http.ResponseWriter, err error) { 73 | switch err { 74 | case domain.ErrInsufficientBalance: 75 | logging.NewError( 76 | t.log, 77 | err, 78 | t.logKey, 79 | http.StatusUnprocessableEntity, 80 | ).Log(t.logMsg) 81 | 82 | response.NewError(err, http.StatusUnprocessableEntity).Send(w) 83 | return 84 | case domain.ErrAccountOriginNotFound: 85 | logging.NewError( 86 | t.log, 87 | err, 88 | t.logKey, 89 | http.StatusUnprocessableEntity, 90 | ).Log(t.logMsg) 91 | 92 | response.NewError(err, http.StatusUnprocessableEntity).Send(w) 93 | return 94 | case domain.ErrAccountDestinationNotFound: 95 | logging.NewError( 96 | t.log, 97 | err, 98 | t.logKey, 99 | http.StatusUnprocessableEntity, 100 | ).Log(t.logMsg) 101 | 102 | response.NewError(err, http.StatusUnprocessableEntity).Send(w) 103 | return 104 | default: 105 | logging.NewError( 106 | t.log, 107 | err, 108 | t.logKey, 109 | http.StatusInternalServerError, 110 | ).Log(t.logMsg) 111 | 112 | response.NewError(err, http.StatusInternalServerError).Send(w) 113 | return 114 | } 115 | } 116 | 117 | func (t CreateTransferAction) validateInput(input usecase.CreateTransferInput) []string { 118 | var ( 119 | msgs []string 120 | errAccountsEquals = errors.New("account origin equals destination account") 121 | accountIsEquals = input.AccountOriginID == input.AccountDestinationID 122 | accountsIsEmpty = input.AccountOriginID == "" && input.AccountDestinationID == "" 123 | ) 124 | 125 | if !accountsIsEmpty && accountIsEquals { 126 | msgs = append(msgs, errAccountsEquals.Error()) 127 | } 128 | 129 | err := t.validator.Validate(input) 130 | if err != nil { 131 | for _, msg := range t.validator.Messages() { 132 | msgs = append(msgs, msg) 133 | } 134 | } 135 | 136 | return msgs 137 | } 138 | -------------------------------------------------------------------------------- /adapter/api/action/create_transfer_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/gsabadini/go-clean-architecture/domain" 14 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 15 | "github.com/gsabadini/go-clean-architecture/infrastructure/validation" 16 | "github.com/gsabadini/go-clean-architecture/usecase" 17 | ) 18 | 19 | type mockCreateTransfer struct { 20 | result usecase.CreateTransferOutput 21 | err error 22 | } 23 | 24 | func (m mockCreateTransfer) Execute(_ context.Context, _ usecase.CreateTransferInput) (usecase.CreateTransferOutput, error) { 25 | return m.result, m.err 26 | } 27 | 28 | func TestCreateTransferAction_Execute(t *testing.T) { 29 | t.Parallel() 30 | 31 | validator, _ := validation.NewValidatorFactory(validation.InstanceGoPlayground) 32 | 33 | type args struct { 34 | rawPayload []byte 35 | } 36 | 37 | tests := []struct { 38 | name string 39 | args args 40 | ucMock usecase.CreateTransferUseCase 41 | expectedBody string 42 | expectedStatusCode int 43 | }{ 44 | { 45 | name: "CreateTransferAction success", 46 | args: args{ 47 | rawPayload: []byte(`{ 48 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 49 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04681", 50 | "amount": 10 51 | }`), 52 | }, 53 | ucMock: mockCreateTransfer{ 54 | result: usecase.CreateTransferOutput{ 55 | ID: "3c096a40-ccba-4b58-93ed-57379ab04679", 56 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 57 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 58 | Amount: 10, 59 | CreatedAt: time.Time{}.String(), 60 | }, 61 | err: nil, 62 | }, 63 | expectedBody: `{"id":"3c096a40-ccba-4b58-93ed-57379ab04679","account_origin_id":"3c096a40-ccba-4b58-93ed-57379ab04680","account_destination_id":"3c096a40-ccba-4b58-93ed-57379ab04681","amount":10,"created_at":"0001-01-01 00:00:00 +0000 UTC"}`, 64 | expectedStatusCode: http.StatusCreated, 65 | }, 66 | { 67 | name: "CreateTransferAction generic error", 68 | args: args{ 69 | rawPayload: []byte( 70 | `{ 71 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 72 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04681", 73 | "amount": 10 74 | }`, 75 | ), 76 | }, 77 | ucMock: mockCreateTransfer{ 78 | result: usecase.CreateTransferOutput{}, 79 | err: errors.New("error"), 80 | }, 81 | expectedBody: `{"errors":["error"]}`, 82 | expectedStatusCode: http.StatusInternalServerError, 83 | }, 84 | { 85 | name: "CreateTransferAction error insufficient balance", 86 | args: args{ 87 | rawPayload: []byte( 88 | `{ 89 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 90 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04681", 91 | "amount": 10 92 | }`, 93 | ), 94 | }, 95 | ucMock: mockCreateTransfer{ 96 | result: usecase.CreateTransferOutput{}, 97 | err: domain.ErrInsufficientBalance, 98 | }, 99 | expectedBody: `{"errors":["origin account does not have sufficient balance"]}`, 100 | expectedStatusCode: http.StatusUnprocessableEntity, 101 | }, 102 | { 103 | name: "CreateTransferAction error not found account origin", 104 | args: args{ 105 | rawPayload: []byte( 106 | `{ 107 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 108 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04681", 109 | "amount": 10 110 | }`, 111 | ), 112 | }, 113 | ucMock: mockCreateTransfer{ 114 | result: usecase.CreateTransferOutput{}, 115 | err: domain.ErrAccountOriginNotFound, 116 | }, 117 | expectedBody: `{"errors":["account origin not found"]}`, 118 | expectedStatusCode: http.StatusUnprocessableEntity, 119 | }, 120 | { 121 | name: "CreateTransferAction error not found account destination", 122 | args: args{ 123 | rawPayload: []byte( 124 | `{ 125 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 126 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04681", 127 | "amount": 10 128 | }`, 129 | ), 130 | }, 131 | ucMock: mockCreateTransfer{ 132 | result: usecase.CreateTransferOutput{}, 133 | err: domain.ErrAccountDestinationNotFound, 134 | }, 135 | expectedBody: `{"errors":["account destination not found"]}`, 136 | expectedStatusCode: http.StatusUnprocessableEntity, 137 | }, 138 | { 139 | name: "CreateTransferAction error account origin equals account destination", 140 | args: args{ 141 | rawPayload: []byte( 142 | `{ 143 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 144 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 145 | "amount": 10 146 | }`, 147 | ), 148 | }, 149 | ucMock: mockCreateTransfer{ 150 | result: usecase.CreateTransferOutput{}, 151 | err: nil, 152 | }, 153 | expectedBody: `{"errors":["account origin equals destination account"]}`, 154 | expectedStatusCode: http.StatusBadRequest, 155 | }, 156 | { 157 | name: "CreateTransferAction error invalid JSON", 158 | args: args{ 159 | rawPayload: []byte( 160 | `{ 161 | "account_destination_id": , 162 | "account_origin_id": , 163 | "amount": 164 | }`, 165 | ), 166 | }, 167 | ucMock: mockCreateTransfer{ 168 | result: usecase.CreateTransferOutput{}, 169 | err: nil, 170 | }, 171 | expectedBody: `{"errors":["invalid character ',' looking for beginning of value"]}`, 172 | expectedStatusCode: http.StatusBadRequest, 173 | }, 174 | { 175 | name: "CreateTransferAction error invalid amount", 176 | args: args{ 177 | rawPayload: []byte( 178 | `{ 179 | "account_destination_id": "3c096a40-ccba-4b58-93ed-57379ab04680", 180 | "account_origin_id": "3c096a40-ccba-4b58-93ed-57379ab04681", 181 | "amount": -1 182 | }`, 183 | ), 184 | }, 185 | ucMock: mockCreateTransfer{ 186 | result: usecase.CreateTransferOutput{}, 187 | err: nil, 188 | }, 189 | expectedBody: `{"errors":["Amount must be greater than 0"]}`, 190 | expectedStatusCode: http.StatusBadRequest, 191 | }, 192 | { 193 | name: "CreateTransferAction error invalid fields", 194 | args: args{ 195 | rawPayload: []byte( 196 | `{ 197 | "account_destination_id123": "3c096a40-ccba-4b58-93ed-57379ab04680", 198 | "account_origin_id123": "3c096a40-ccba-4b58-93ed-57379ab04681", 199 | "amount123": 10 200 | }`, 201 | ), 202 | }, 203 | ucMock: mockCreateTransfer{ 204 | result: usecase.CreateTransferOutput{}, 205 | err: nil, 206 | }, 207 | expectedBody: `{"errors":["AccountOriginID is a required field","AccountDestinationID is a required field","Amount must be greater than 0"]}`, 208 | expectedStatusCode: http.StatusBadRequest, 209 | }, 210 | } 211 | 212 | for _, tt := range tests { 213 | t.Run(tt.name, func(t *testing.T) { 214 | req, _ := http.NewRequest( 215 | http.MethodPost, 216 | "/transfers", 217 | bytes.NewReader(tt.args.rawPayload), 218 | ) 219 | 220 | var ( 221 | w = httptest.NewRecorder() 222 | action = NewCreateTransferAction(tt.ucMock, log.LoggerMock{}, validator) 223 | ) 224 | 225 | action.Execute(w, req) 226 | 227 | if w.Code != tt.expectedStatusCode { 228 | t.Errorf( 229 | "[TestCase '%s'] O handler retornou um HTTP status code inesperado: retornado '%v' esperado '%v'", 230 | tt.name, 231 | w.Code, 232 | tt.expectedStatusCode, 233 | ) 234 | } 235 | 236 | var result = strings.TrimSpace(w.Body.String()) 237 | if !strings.EqualFold(result, tt.expectedBody) { 238 | t.Errorf( 239 | "[TestCase '%s'] Result: '%v' | Expected: '%v'", 240 | tt.name, 241 | result, 242 | tt.expectedBody, 243 | ) 244 | } 245 | }) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /adapter/api/action/find_account_balance.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/api/logging" 7 | "github.com/gsabadini/go-clean-architecture/adapter/api/response" 8 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 9 | "github.com/gsabadini/go-clean-architecture/domain" 10 | "github.com/gsabadini/go-clean-architecture/usecase" 11 | ) 12 | 13 | type FindAccountBalanceAction struct { 14 | uc usecase.FindAccountBalanceUseCase 15 | log logger.Logger 16 | } 17 | 18 | func NewFindAccountBalanceAction(uc usecase.FindAccountBalanceUseCase, log logger.Logger) FindAccountBalanceAction { 19 | return FindAccountBalanceAction{ 20 | uc: uc, 21 | log: log, 22 | } 23 | } 24 | 25 | func (a FindAccountBalanceAction) Execute(w http.ResponseWriter, r *http.Request) { 26 | const logKey = "find_balance_account" 27 | 28 | var accountID = r.URL.Query().Get("account_id") 29 | if !domain.IsValidUUID(accountID) { 30 | var err = response.ErrParameterInvalid 31 | logging.NewError( 32 | a.log, 33 | err, 34 | logKey, 35 | http.StatusBadRequest, 36 | ).Log("invalid parameter") 37 | 38 | response.NewError(err, http.StatusBadRequest).Send(w) 39 | return 40 | } 41 | 42 | output, err := a.uc.Execute(r.Context(), domain.AccountID(accountID)) 43 | if err != nil { 44 | switch err { 45 | case domain.ErrAccountNotFound: 46 | logging.NewError( 47 | a.log, 48 | err, 49 | logKey, 50 | http.StatusBadRequest, 51 | ).Log("error fetching account balance") 52 | 53 | response.NewError(err, http.StatusBadRequest).Send(w) 54 | return 55 | default: 56 | logging.NewError( 57 | a.log, 58 | err, 59 | logKey, 60 | http.StatusInternalServerError, 61 | ).Log("error when returning account balance") 62 | 63 | response.NewError(err, http.StatusInternalServerError).Send(w) 64 | return 65 | } 66 | } 67 | logging.NewInfo(a.log, logKey, http.StatusOK).Log("success when returning account balance") 68 | 69 | response.NewSuccess(output, http.StatusOK).Send(w) 70 | } 71 | -------------------------------------------------------------------------------- /adapter/api/action/find_account_balance_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/gsabadini/go-clean-architecture/domain" 13 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 14 | "github.com/gsabadini/go-clean-architecture/usecase" 15 | ) 16 | 17 | type mockFindBalanceAccount struct { 18 | result usecase.FindAccountBalanceOutput 19 | err error 20 | } 21 | 22 | func (m mockFindBalanceAccount) Execute(_ context.Context, _ domain.AccountID) (usecase.FindAccountBalanceOutput, error) { 23 | return m.result, m.err 24 | } 25 | 26 | func TestFindAccountBalanceAction_Execute(t *testing.T) { 27 | t.Parallel() 28 | 29 | type args struct { 30 | accountID string 31 | } 32 | 33 | tests := []struct { 34 | name string 35 | args args 36 | ucMock usecase.FindAccountBalanceUseCase 37 | expectedBody string 38 | expectedStatusCode int 39 | }{ 40 | { 41 | name: "FindAccountBalanceAction success", 42 | args: args{ 43 | accountID: "3c096a40-ccba-4b58-93ed-57379ab04680", 44 | }, 45 | ucMock: mockFindBalanceAccount{ 46 | result: usecase.FindAccountBalanceOutput{ 47 | Balance: 10, 48 | }, 49 | err: nil, 50 | }, 51 | expectedBody: `{"balance":10}`, 52 | expectedStatusCode: http.StatusOK, 53 | }, 54 | { 55 | name: "FindAccountBalanceAction generic error", 56 | args: args{ 57 | accountID: "3c096a40-ccba-4b58-93ed-57379ab04680", 58 | }, 59 | ucMock: mockFindBalanceAccount{ 60 | result: usecase.FindAccountBalanceOutput{}, 61 | err: errors.New("error"), 62 | }, 63 | expectedBody: `{"errors":["error"]}`, 64 | expectedStatusCode: http.StatusInternalServerError, 65 | }, 66 | { 67 | name: "FindAccountBalanceAction error parameter invalid", 68 | args: args{ 69 | accountID: "error", 70 | }, 71 | ucMock: mockFindBalanceAccount{ 72 | result: usecase.FindAccountBalanceOutput{}, 73 | err: nil, 74 | }, 75 | expectedBody: `{"errors":["parameter invalid"]}`, 76 | expectedStatusCode: http.StatusBadRequest, 77 | }, 78 | { 79 | name: "FindAccountBalanceAction error fetching account", 80 | args: args{ 81 | accountID: "3c096a40-ccba-4b58-93ed-57379ab04680", 82 | }, 83 | ucMock: mockFindBalanceAccount{ 84 | result: usecase.FindAccountBalanceOutput{}, 85 | err: domain.ErrAccountNotFound, 86 | }, 87 | expectedBody: `{"errors":["account not found"]}`, 88 | expectedStatusCode: http.StatusBadRequest, 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | uri := fmt.Sprintf("/accounts/%s/balance", tt.args.accountID) 95 | req, _ := http.NewRequest(http.MethodGet, uri, nil) 96 | 97 | q := req.URL.Query() 98 | q.Add("account_id", tt.args.accountID) 99 | req.URL.RawQuery = q.Encode() 100 | 101 | var ( 102 | w = httptest.NewRecorder() 103 | action = NewFindAccountBalanceAction(tt.ucMock, log.LoggerMock{}) 104 | ) 105 | 106 | action.Execute(w, req) 107 | 108 | if w.Code != tt.expectedStatusCode { 109 | t.Errorf( 110 | "[TestCase '%s'] O handler retornou um HTTP status code inesperado: retornado '%v' esperado '%v'", 111 | tt.name, 112 | w.Code, 113 | tt.expectedStatusCode, 114 | ) 115 | } 116 | 117 | var result = strings.TrimSpace(w.Body.String()) 118 | if !strings.EqualFold(result, tt.expectedBody) { 119 | t.Errorf( 120 | "[TestCase '%s'] Result: '%v' | Expected: '%v'", 121 | tt.name, 122 | result, 123 | tt.expectedBody, 124 | ) 125 | } 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /adapter/api/action/find_all_account.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/api/logging" 7 | "github.com/gsabadini/go-clean-architecture/adapter/api/response" 8 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 9 | "github.com/gsabadini/go-clean-architecture/usecase" 10 | ) 11 | 12 | type FindAllAccountAction struct { 13 | uc usecase.FindAllAccountUseCase 14 | log logger.Logger 15 | } 16 | 17 | func NewFindAllAccountAction(uc usecase.FindAllAccountUseCase, log logger.Logger) FindAllAccountAction { 18 | return FindAllAccountAction{ 19 | uc: uc, 20 | log: log, 21 | } 22 | } 23 | 24 | func (a FindAllAccountAction) Execute(w http.ResponseWriter, r *http.Request) { 25 | const logKey = "find_all_account" 26 | 27 | output, err := a.uc.Execute(r.Context()) 28 | if err != nil { 29 | logging.NewError( 30 | a.log, 31 | err, 32 | logKey, 33 | http.StatusInternalServerError, 34 | ).Log("error when returning account list") 35 | 36 | response.NewError(err, http.StatusInternalServerError).Send(w) 37 | return 38 | } 39 | logging.NewInfo(a.log, logKey, http.StatusOK).Log("success when returning account list") 40 | 41 | response.NewSuccess(output, http.StatusOK).Send(w) 42 | } 43 | -------------------------------------------------------------------------------- /adapter/api/action/find_all_account_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 13 | "github.com/gsabadini/go-clean-architecture/usecase" 14 | ) 15 | 16 | type mockFindAllAccount struct { 17 | result []usecase.FindAllAccountOutput 18 | err error 19 | } 20 | 21 | func (m mockFindAllAccount) Execute(_ context.Context) ([]usecase.FindAllAccountOutput, error) { 22 | return m.result, m.err 23 | } 24 | 25 | func TestFindAllAccountAction_Execute(t *testing.T) { 26 | t.Parallel() 27 | 28 | tests := []struct { 29 | name string 30 | ucMock usecase.FindAllAccountUseCase 31 | expectedBody string 32 | expectedStatusCode int 33 | }{ 34 | { 35 | name: "FindAllAccountAction success one account", 36 | ucMock: mockFindAllAccount{ 37 | result: []usecase.FindAllAccountOutput{ 38 | { 39 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 40 | Name: "Test", 41 | CPF: "07094564964", 42 | Balance: 10, 43 | CreatedAt: time.Time{}.String(), 44 | }, 45 | }, 46 | err: nil, 47 | }, 48 | expectedBody: `[{"id":"3c096a40-ccba-4b58-93ed-57379ab04680","name":"Test","cpf":"07094564964","balance":10,"created_at":"0001-01-01 00:00:00 +0000 UTC"}]`, 49 | expectedStatusCode: http.StatusOK, 50 | }, 51 | { 52 | name: "FindAllAccountAction success empty", 53 | ucMock: mockFindAllAccount{ 54 | result: []usecase.FindAllAccountOutput{}, 55 | err: nil, 56 | }, 57 | expectedBody: `[]`, 58 | expectedStatusCode: http.StatusOK, 59 | }, 60 | { 61 | name: "FindAllAccountAction generic error", 62 | ucMock: mockFindAllAccount{ 63 | err: errors.New("error"), 64 | }, 65 | expectedBody: `{"errors":["error"]}`, 66 | expectedStatusCode: http.StatusInternalServerError, 67 | }, 68 | } 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | req, _ := http.NewRequest(http.MethodGet, "/accounts", nil) 73 | 74 | var ( 75 | w = httptest.NewRecorder() 76 | action = NewFindAllAccountAction(tt.ucMock, log.LoggerMock{}) 77 | ) 78 | 79 | action.Execute(w, req) 80 | 81 | if w.Code != tt.expectedStatusCode { 82 | t.Errorf( 83 | "[TestCase '%s'] O handler retornou um HTTP status code inesperado: retornado '%v' esperado '%v'", 84 | tt.name, 85 | w.Code, 86 | tt.expectedStatusCode, 87 | ) 88 | } 89 | 90 | var result = strings.TrimSpace(w.Body.String()) 91 | if !strings.EqualFold(result, tt.expectedBody) { 92 | t.Errorf( 93 | "[TestCase '%s'] Result: '%v' | Expected: '%v'", 94 | tt.name, 95 | result, 96 | tt.expectedBody, 97 | ) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /adapter/api/action/find_all_transfer.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/api/logging" 7 | "github.com/gsabadini/go-clean-architecture/adapter/api/response" 8 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 9 | "github.com/gsabadini/go-clean-architecture/usecase" 10 | ) 11 | 12 | type FindAllTransferAction struct { 13 | uc usecase.FindAllTransferUseCase 14 | log logger.Logger 15 | } 16 | 17 | func NewFindAllTransferAction(uc usecase.FindAllTransferUseCase, log logger.Logger) FindAllTransferAction { 18 | return FindAllTransferAction{ 19 | uc: uc, 20 | log: log, 21 | } 22 | } 23 | 24 | func (t FindAllTransferAction) Execute(w http.ResponseWriter, r *http.Request) { 25 | const logKey = "find_all_transfer" 26 | 27 | output, err := t.uc.Execute(r.Context()) 28 | if err != nil { 29 | logging.NewError( 30 | t.log, 31 | err, 32 | logKey, 33 | http.StatusInternalServerError, 34 | ).Log("error when returning the transfer list") 35 | 36 | response.NewError(err, http.StatusInternalServerError).Send(w) 37 | return 38 | } 39 | logging.NewInfo(t.log, logKey, http.StatusOK).Log("success when returning transfer list") 40 | 41 | response.NewSuccess(output, http.StatusOK).Send(w) 42 | } 43 | -------------------------------------------------------------------------------- /adapter/api/action/find_all_transfer_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 13 | "github.com/gsabadini/go-clean-architecture/usecase" 14 | ) 15 | 16 | type mockFindAllTransfer struct { 17 | result []usecase.FindAllTransferOutput 18 | err error 19 | } 20 | 21 | func (m mockFindAllTransfer) Execute(_ context.Context) ([]usecase.FindAllTransferOutput, error) { 22 | return m.result, m.err 23 | } 24 | 25 | func TestTransfer_Index(t *testing.T) { 26 | t.Parallel() 27 | 28 | tests := []struct { 29 | name string 30 | ucMock usecase.FindAllTransferUseCase 31 | expectedBody string 32 | expectedStatusCode int 33 | }{ 34 | { 35 | name: "FindAllTransferAction success one transfer", 36 | ucMock: mockFindAllTransfer{ 37 | result: []usecase.FindAllTransferOutput{ 38 | { 39 | ID: "3c096a40-ccba-4b58-93ed-57379ab04679", 40 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 41 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 42 | Amount: 10, 43 | CreatedAt: time.Time{}.String(), 44 | }, 45 | }, 46 | err: nil, 47 | }, 48 | expectedBody: `[{"id":"3c096a40-ccba-4b58-93ed-57379ab04679","account_origin_id":"3c096a40-ccba-4b58-93ed-57379ab04680","account_destination_id":"3c096a40-ccba-4b58-93ed-57379ab04681","amount":10,"created_at":"0001-01-01 00:00:00 +0000 UTC"}]`, 49 | expectedStatusCode: http.StatusOK, 50 | }, 51 | { 52 | name: "FindAllTransferAction success empty", 53 | ucMock: mockFindAllTransfer{ 54 | result: []usecase.FindAllTransferOutput{}, 55 | err: nil, 56 | }, 57 | expectedBody: `[]`, 58 | expectedStatusCode: http.StatusOK, 59 | }, 60 | { 61 | name: "FindAllTransferAction generic error", 62 | ucMock: mockFindAllTransfer{ 63 | err: errors.New("error"), 64 | }, 65 | expectedBody: `{"errors":["error"]}`, 66 | expectedStatusCode: http.StatusInternalServerError, 67 | }, 68 | } 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | req, _ := http.NewRequest(http.MethodGet, "/transfers", nil) 73 | 74 | var ( 75 | w = httptest.NewRecorder() 76 | action = NewFindAllTransferAction(tt.ucMock, log.LoggerMock{}) 77 | ) 78 | 79 | action.Execute(w, req) 80 | 81 | if w.Code != tt.expectedStatusCode { 82 | t.Errorf( 83 | "[TestCase '%s'] O handler retornou um HTTP status code inesperado: retornado '%v' esperado '%v'", 84 | tt.name, 85 | w.Code, 86 | tt.expectedStatusCode, 87 | ) 88 | } 89 | 90 | var result = strings.TrimSpace(w.Body.String()) 91 | if !strings.EqualFold(result, tt.expectedBody) { 92 | t.Errorf( 93 | "[TestCase '%s'] Result: '%v' | Expected: '%v'", 94 | tt.name, 95 | result, 96 | tt.expectedBody, 97 | ) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /adapter/api/action/health_check.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import "net/http" 4 | 5 | func HealthCheck(w http.ResponseWriter, _ *http.Request) { 6 | w.WriteHeader(http.StatusOK) 7 | } 8 | -------------------------------------------------------------------------------- /adapter/api/action/health_check_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestHealthCheck(t *testing.T) { 10 | t.Parallel() 11 | 12 | req, err := http.NewRequest(http.MethodGet, "/healthcheck", nil) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | var ( 18 | rr = httptest.NewRecorder() 19 | handler = http.NewServeMux() 20 | ) 21 | 22 | handler.HandleFunc("/healthcheck", HealthCheck) 23 | handler.ServeHTTP(rr, req) 24 | 25 | if status := rr.Code; status != http.StatusOK { 26 | t.Errorf("O handler retornou um HTTP status code inesperado: retornado '%v' esperado '%v'", 27 | status, 28 | http.StatusOK, 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /adapter/api/logging/error.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 5 | ) 6 | 7 | type Error struct { 8 | log logger.Logger 9 | err error 10 | key string 11 | httpStatus int 12 | } 13 | 14 | func NewError(log logger.Logger, err error, key string, httpStatus int) Error { 15 | return Error{ 16 | log: log, 17 | err: err, 18 | key: key, 19 | httpStatus: httpStatus, 20 | } 21 | } 22 | 23 | func (e Error) Log(msg string) { 24 | e.log.WithFields(logger.Fields{ 25 | "key": e.key, 26 | "error": e.err.Error(), 27 | "http_status": e.httpStatus, 28 | }).Errorf(msg) 29 | } 30 | -------------------------------------------------------------------------------- /adapter/api/logging/info.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 5 | ) 6 | 7 | type Info struct { 8 | log logger.Logger 9 | key string 10 | httpStatus int 11 | } 12 | 13 | func NewInfo(log logger.Logger, key string, httpStatus int) Info { 14 | return Info{ 15 | log: log, 16 | key: key, 17 | httpStatus: httpStatus, 18 | } 19 | } 20 | 21 | func (i Info) Log(msg string) { 22 | i.log.WithFields(logger.Fields{ 23 | "key": i.key, 24 | "http_status": i.httpStatus, 25 | }).Infof(msg) 26 | } 27 | -------------------------------------------------------------------------------- /adapter/api/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/gsabadini/go-clean-architecture/adapter/api/logging" 11 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/urfave/negroni" 15 | ) 16 | 17 | type Logger struct { 18 | log logger.Logger 19 | } 20 | 21 | func NewLogger(log logger.Logger) Logger { 22 | return Logger{log: log} 23 | } 24 | 25 | func (l Logger) Execute(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 26 | start := time.Now() 27 | 28 | const ( 29 | logKey = "logger_middleware" 30 | requestKey = "api_request" 31 | responseKey = "api_response" 32 | ) 33 | 34 | body, err := getRequestPayload(r) 35 | if err != nil { 36 | logging.NewError( 37 | l.log, 38 | err, 39 | logKey, 40 | http.StatusBadRequest, 41 | ).Log("error when getting payload") 42 | 43 | return 44 | } 45 | 46 | l.log.WithFields(logger.Fields{ 47 | "key": requestKey, 48 | "payload": body, 49 | "url": r.URL.Path, 50 | "http_method": r.Method, 51 | }).Infof("started handling request") 52 | 53 | next.ServeHTTP(w, r) 54 | 55 | end := time.Since(start).Seconds() 56 | res := w.(negroni.ResponseWriter) 57 | l.log.WithFields(logger.Fields{ 58 | "key": responseKey, 59 | "url": r.URL.Path, 60 | "http_method": r.Method, 61 | "http_status": res.Status(), 62 | "response_time": end, 63 | }).Infof("completed handling request") 64 | } 65 | 66 | func getRequestPayload(r *http.Request) (string, error) { 67 | if r.Body == nil { 68 | return "", errors.New("body not defined") 69 | } 70 | 71 | payload, err := ioutil.ReadAll(r.Body) 72 | if err != nil { 73 | return "", errors.Wrap(err, "error read body") 74 | } 75 | 76 | r.Body = ioutil.NopCloser(bytes.NewBuffer(payload)) 77 | 78 | return strings.TrimSpace(string(payload)), nil 79 | } 80 | -------------------------------------------------------------------------------- /adapter/api/response/error.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | "net/http" 7 | ) 8 | 9 | var ( 10 | ErrParameterInvalid = errors.New("parameter invalid") 11 | 12 | ErrInvalidInput = errors.New("invalid input") 13 | ) 14 | 15 | type Error struct { 16 | statusCode int 17 | Errors []string `json:"errors"` 18 | } 19 | 20 | func NewError(err error, status int) *Error { 21 | return &Error{ 22 | statusCode: status, 23 | Errors: []string{err.Error()}, 24 | } 25 | } 26 | 27 | func NewErrorMessage(messages []string, status int) *Error { 28 | return &Error{ 29 | statusCode: status, 30 | Errors: messages, 31 | } 32 | } 33 | 34 | func (e Error) Send(w http.ResponseWriter) error { 35 | w.Header().Set("Content-Type", "application/json") 36 | w.WriteHeader(e.statusCode) 37 | return json.NewEncoder(w).Encode(e) 38 | } 39 | -------------------------------------------------------------------------------- /adapter/api/response/success.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type Success struct { 9 | statusCode int 10 | result interface{} 11 | } 12 | 13 | func NewSuccess(result interface{}, status int) Success { 14 | return Success{ 15 | statusCode: status, 16 | result: result, 17 | } 18 | } 19 | 20 | func (r Success) Send(w http.ResponseWriter) error { 21 | w.Header().Set("Content-Type", "application/json") 22 | w.WriteHeader(r.statusCode) 23 | return json.NewEncoder(w).Encode(r.result) 24 | } 25 | -------------------------------------------------------------------------------- /adapter/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | type Logger interface { 4 | Infof(format string, args ...interface{}) 5 | Warnf(format string, args ...interface{}) 6 | Errorf(format string, args ...interface{}) 7 | Fatalln(args ...interface{}) 8 | WithFields(keyValues Fields) Logger 9 | WithError(err error) Logger 10 | } 11 | 12 | type Fields map[string]interface{} 13 | -------------------------------------------------------------------------------- /adapter/presenter/create_account.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gsabadini/go-clean-architecture/domain" 7 | "github.com/gsabadini/go-clean-architecture/usecase" 8 | ) 9 | 10 | type createAccountPresenter struct{} 11 | 12 | func NewCreateAccountPresenter() usecase.CreateAccountPresenter { 13 | return createAccountPresenter{} 14 | } 15 | 16 | func (a createAccountPresenter) Output(account domain.Account) usecase.CreateAccountOutput { 17 | return usecase.CreateAccountOutput{ 18 | ID: account.ID().String(), 19 | Name: account.Name(), 20 | CPF: account.CPF(), 21 | Balance: account.Balance().Float64(), 22 | CreatedAt: account.CreatedAt().Format(time.RFC3339), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /adapter/presenter/create_account_test.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/domain" 5 | "github.com/gsabadini/go-clean-architecture/usecase" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_createAccountPresenter_Output(t *testing.T) { 12 | type args struct { 13 | account domain.Account 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want usecase.CreateAccountOutput 19 | }{ 20 | { 21 | name: "Create account output", 22 | args: args{ 23 | account: domain.NewAccount( 24 | "3c096a40-ccba-4b58-93ed-57379ab04680", 25 | "Testing", 26 | "07091054965", 27 | 1000, 28 | time.Time{}, 29 | ), 30 | }, 31 | want: usecase.CreateAccountOutput{ 32 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 33 | Name: "Testing", 34 | CPF: "07091054965", 35 | Balance: 10, 36 | CreatedAt: "0001-01-01T00:00:00Z", 37 | }, 38 | }, 39 | } 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | pre := NewCreateAccountPresenter() 43 | if got := pre.Output(tt.args.account); !reflect.DeepEqual(got, tt.want) { 44 | t.Errorf("[TestCase '%s'] Got: '%+v' | Want: '%+v'", tt.name, got, tt.want) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /adapter/presenter/create_transfer.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gsabadini/go-clean-architecture/domain" 7 | "github.com/gsabadini/go-clean-architecture/usecase" 8 | ) 9 | 10 | type createTransferPresenter struct{} 11 | 12 | func NewCreateTransferPresenter() usecase.CreateTransferPresenter { 13 | return createTransferPresenter{} 14 | } 15 | 16 | func (c createTransferPresenter) Output(transfer domain.Transfer) usecase.CreateTransferOutput { 17 | return usecase.CreateTransferOutput{ 18 | ID: transfer.ID().String(), 19 | AccountOriginID: transfer.AccountOriginID().String(), 20 | AccountDestinationID: transfer.AccountDestinationID().String(), 21 | Amount: transfer.Amount().Float64(), 22 | CreatedAt: transfer.CreatedAt().Format(time.RFC3339), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /adapter/presenter/create_transfer_test.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gsabadini/go-clean-architecture/domain" 9 | "github.com/gsabadini/go-clean-architecture/usecase" 10 | ) 11 | 12 | func Test_createTransferPresenter_Output(t *testing.T) { 13 | type args struct { 14 | transfer domain.Transfer 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want usecase.CreateTransferOutput 20 | }{ 21 | { 22 | name: "Create transfer output", 23 | args: args{ 24 | transfer: domain.NewTransfer( 25 | "3c096a40-ccba-4b58-93ed-57379ab04680", 26 | "3c096a40-ccba-4b58-93ed-57379ab04681", 27 | "3c096a40-ccba-4b58-93ed-57379ab04682", 28 | 1000, 29 | time.Time{}, 30 | ), 31 | }, 32 | want: usecase.CreateTransferOutput{ 33 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 34 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 35 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 36 | Amount: 10, 37 | CreatedAt: "0001-01-01T00:00:00Z", 38 | }, 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | pre := NewCreateTransferPresenter() 44 | if got := pre.Output(tt.args.transfer); !reflect.DeepEqual(got, tt.want) { 45 | t.Errorf("[TestCase '%s'] Got: '%+v' | Want: '%+v'", tt.name, got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /adapter/presenter/find_account_balance.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/domain" 5 | "github.com/gsabadini/go-clean-architecture/usecase" 6 | ) 7 | 8 | type findAccountBalancePresenter struct{} 9 | 10 | func NewFindAccountBalancePresenter() usecase.FindAccountBalancePresenter { 11 | return findAccountBalancePresenter{} 12 | } 13 | 14 | func (a findAccountBalancePresenter) Output(balance domain.Money) usecase.FindAccountBalanceOutput { 15 | return usecase.FindAccountBalanceOutput{Balance: balance.Float64()} 16 | } 17 | -------------------------------------------------------------------------------- /adapter/presenter/find_account_balance_test.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/domain" 5 | "github.com/gsabadini/go-clean-architecture/usecase" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestNewFindAccountBalancePresenter(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | want usecase.FindAccountBalancePresenter 14 | }{ 15 | // TODO: Add test cases. 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | if got := NewFindAccountBalancePresenter(); !reflect.DeepEqual(got, tt.want) { 20 | t.Errorf("NewFindAccountBalancePresenter() = %v, want %v", got, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | 26 | func Test_findAccountBalancePresenter_Output(t *testing.T) { 27 | type args struct { 28 | balance domain.Money 29 | } 30 | tests := []struct { 31 | name string 32 | args args 33 | want usecase.FindAccountBalanceOutput 34 | }{ 35 | { 36 | name: "Find account balance ouitput", 37 | args: args{ 38 | balance: 1099, 39 | }, 40 | want: usecase.FindAccountBalanceOutput{ 41 | Balance: 10.99, 42 | }, 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | pre := NewFindAccountBalancePresenter() 48 | if got := pre.Output(tt.args.balance); !reflect.DeepEqual(got, tt.want) { 49 | t.Errorf("[TestCase '%s'] Got: '%+v' | Want: '%+v'", tt.name, got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /adapter/presenter/find_all_account.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gsabadini/go-clean-architecture/domain" 7 | "github.com/gsabadini/go-clean-architecture/usecase" 8 | ) 9 | 10 | type findAllAccountPresenter struct{} 11 | 12 | func NewFindAllAccountPresenter() usecase.FindAllAccountPresenter { 13 | return findAllAccountPresenter{} 14 | } 15 | 16 | func (a findAllAccountPresenter) Output(accounts []domain.Account) []usecase.FindAllAccountOutput { 17 | var o = make([]usecase.FindAllAccountOutput, 0) 18 | 19 | for _, account := range accounts { 20 | o = append(o, usecase.FindAllAccountOutput{ 21 | ID: account.ID().String(), 22 | Name: account.Name(), 23 | CPF: account.CPF(), 24 | Balance: account.Balance().Float64(), 25 | CreatedAt: account.CreatedAt().Format(time.RFC3339), 26 | }) 27 | } 28 | 29 | return o 30 | } 31 | -------------------------------------------------------------------------------- /adapter/presenter/find_all_account_test.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gsabadini/go-clean-architecture/domain" 9 | "github.com/gsabadini/go-clean-architecture/usecase" 10 | ) 11 | 12 | func Test_findAllAccountPresenter_Output(t *testing.T) { 13 | type args struct { 14 | accounts []domain.Account 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want []usecase.FindAllAccountOutput 20 | }{ 21 | { 22 | name: "Find all account output", 23 | args: args{ 24 | accounts: []domain.Account{ 25 | domain.NewAccount( 26 | "3c096a40-ccba-4b58-93ed-57379ab04680", 27 | "Testing", 28 | "07091054965", 29 | 1000, 30 | time.Time{}, 31 | ), 32 | domain.NewAccount( 33 | "3c096a40-ccba-4b58-93ed-57379ab04682", 34 | "Testing", 35 | "07091054965", 36 | 99, 37 | time.Time{}, 38 | ), 39 | }, 40 | }, 41 | want: []usecase.FindAllAccountOutput{ 42 | { 43 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 44 | Name: "Testing", 45 | CPF: "07091054965", 46 | Balance: 10, 47 | CreatedAt: "0001-01-01T00:00:00Z", 48 | }, 49 | { 50 | ID: "3c096a40-ccba-4b58-93ed-57379ab04682", 51 | Name: "Testing", 52 | CPF: "07091054965", 53 | Balance: 0.99, 54 | CreatedAt: "0001-01-01T00:00:00Z", 55 | }, 56 | }, 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | pre := NewFindAllAccountPresenter() 62 | if got := pre.Output(tt.args.accounts); !reflect.DeepEqual(got, tt.want) { 63 | t.Errorf("[TestCase '%s'] Got: '%+v' | Want: '%+v'", tt.name, got, tt.want) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /adapter/presenter/find_all_transfer.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gsabadini/go-clean-architecture/domain" 7 | "github.com/gsabadini/go-clean-architecture/usecase" 8 | ) 9 | 10 | type findAllTransferPresenter struct{} 11 | 12 | func NewFindAllTransferPresenter() usecase.FindAllTransferPresenter { 13 | return findAllTransferPresenter{} 14 | } 15 | 16 | func (a findAllTransferPresenter) Output(transfers []domain.Transfer) []usecase.FindAllTransferOutput { 17 | var o = make([]usecase.FindAllTransferOutput, 0) 18 | 19 | for _, transfer := range transfers { 20 | o = append(o, usecase.FindAllTransferOutput{ 21 | ID: transfer.ID().String(), 22 | AccountOriginID: transfer.AccountOriginID().String(), 23 | AccountDestinationID: transfer.AccountDestinationID().String(), 24 | Amount: transfer.Amount().Float64(), 25 | CreatedAt: transfer.CreatedAt().Format(time.RFC3339), 26 | }) 27 | } 28 | 29 | return o 30 | } 31 | -------------------------------------------------------------------------------- /adapter/presenter/find_all_transfer_test.go: -------------------------------------------------------------------------------- 1 | package presenter 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/domain" 5 | "github.com/gsabadini/go-clean-architecture/usecase" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_findAllTransferPresenter_Output(t *testing.T) { 12 | type args struct { 13 | transfers []domain.Transfer 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want []usecase.FindAllTransferOutput 19 | }{ 20 | { 21 | name: "Find all transfer output", 22 | args: args{ 23 | transfers: []domain.Transfer{ 24 | domain.NewTransfer( 25 | "3c096a40-ccba-4b58-93ed-57379ab04680", 26 | "3c096a40-ccba-4b58-93ed-57379ab04681", 27 | "3c096a40-ccba-4b58-93ed-57379ab04682", 28 | 1000, 29 | time.Time{}, 30 | ), 31 | domain.NewTransfer( 32 | "3c096a40-ccba-4b58-93ed-57379ab04680", 33 | "3c096a40-ccba-4b58-93ed-57379ab04681", 34 | "3c096a40-ccba-4b58-93ed-57379ab04682", 35 | 99, 36 | time.Time{}, 37 | ), 38 | }, 39 | }, 40 | want: []usecase.FindAllTransferOutput{ 41 | { 42 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 43 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 44 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 45 | Amount: 10, 46 | CreatedAt: "0001-01-01T00:00:00Z", 47 | }, 48 | { 49 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 50 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 51 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 52 | Amount: 0.99, 53 | CreatedAt: "0001-01-01T00:00:00Z", 54 | }, 55 | }, 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | pre := NewFindAllTransferPresenter() 61 | if got := pre.Output(tt.args.transfers); !reflect.DeepEqual(got, tt.want) { 62 | t.Errorf("[TestCase '%s'] Got: '%+v' | Want: '%+v'", tt.name, got, tt.want) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /adapter/repository/account_mongodb.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | "github.com/pkg/errors" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | type accountBSON struct { 14 | ID string `bson:"id"` 15 | Name string `bson:"name"` 16 | CPF string `bson:"cpf"` 17 | Balance int64 `bson:"balance"` 18 | CreatedAt time.Time `bson:"created_at"` 19 | } 20 | 21 | type AccountNoSQL struct { 22 | collectionName string 23 | db NoSQL 24 | } 25 | 26 | func NewAccountNoSQL(db NoSQL) AccountNoSQL { 27 | return AccountNoSQL{ 28 | db: db, 29 | collectionName: "accounts", 30 | } 31 | } 32 | 33 | func (a AccountNoSQL) Create(ctx context.Context, account domain.Account) (domain.Account, error) { 34 | var accountBSON = accountBSON{ 35 | ID: account.ID().String(), 36 | Name: account.Name(), 37 | CPF: account.CPF(), 38 | Balance: account.Balance().Int64(), 39 | CreatedAt: account.CreatedAt(), 40 | } 41 | 42 | if err := a.db.Store(ctx, a.collectionName, accountBSON); err != nil { 43 | return domain.Account{}, errors.Wrap(err, "error creating account") 44 | } 45 | 46 | return account, nil 47 | } 48 | 49 | func (a AccountNoSQL) UpdateBalance(ctx context.Context, ID domain.AccountID, balance domain.Money) error { 50 | var ( 51 | query = bson.M{"id": ID} 52 | update = bson.M{"$set": bson.M{"balance": balance}} 53 | ) 54 | 55 | if err := a.db.Update(ctx, a.collectionName, query, update); err != nil { 56 | switch err { 57 | case mongo.ErrNilDocument: 58 | return errors.Wrap(domain.ErrAccountNotFound, "error updating account balance") 59 | default: 60 | return errors.Wrap(err, "error updating account balance") 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (a AccountNoSQL) FindAll(ctx context.Context) ([]domain.Account, error) { 68 | var accountsBSON = make([]accountBSON, 0) 69 | 70 | if err := a.db.FindAll(ctx, a.collectionName, bson.M{}, &accountsBSON); err != nil { 71 | switch err { 72 | case mongo.ErrNilDocument: 73 | return []domain.Account{}, errors.Wrap(domain.ErrAccountNotFound, "error listing accounts") 74 | default: 75 | return []domain.Account{}, errors.Wrap(err, "error listing accounts") 76 | } 77 | } 78 | 79 | var accounts = make([]domain.Account, 0) 80 | 81 | for _, accountBSON := range accountsBSON { 82 | var account = domain.NewAccount( 83 | domain.AccountID(accountBSON.ID), 84 | accountBSON.Name, 85 | accountBSON.CPF, 86 | domain.Money(accountBSON.Balance), 87 | accountBSON.CreatedAt, 88 | ) 89 | 90 | accounts = append(accounts, account) 91 | } 92 | 93 | return accounts, nil 94 | } 95 | 96 | func (a AccountNoSQL) FindByID(ctx context.Context, ID domain.AccountID) (domain.Account, error) { 97 | var ( 98 | accountBSON = &accountBSON{} 99 | query = bson.M{"id": ID} 100 | ) 101 | 102 | if err := a.db.FindOne(ctx, a.collectionName, query, nil, accountBSON); err != nil { 103 | switch err { 104 | case mongo.ErrNoDocuments: 105 | return domain.Account{}, domain.ErrAccountNotFound 106 | default: 107 | return domain.Account{}, errors.Wrap(err, "error fetching account") 108 | } 109 | } 110 | 111 | return domain.NewAccount( 112 | domain.AccountID(accountBSON.ID), 113 | accountBSON.Name, 114 | accountBSON.CPF, 115 | domain.Money(accountBSON.Balance), 116 | accountBSON.CreatedAt, 117 | ), nil 118 | } 119 | 120 | func (a AccountNoSQL) FindBalance(ctx context.Context, ID domain.AccountID) (domain.Account, error) { 121 | var ( 122 | accountBSON = &accountBSON{} 123 | query = bson.M{"id": ID} 124 | projection = bson.M{"balance": 1, "_id": 0} 125 | ) 126 | 127 | if err := a.db.FindOne(ctx, a.collectionName, query, projection, accountBSON); err != nil { 128 | switch err { 129 | case mongo.ErrNoDocuments: 130 | return domain.Account{}, domain.ErrAccountNotFound 131 | default: 132 | return domain.Account{}, errors.Wrap(err, "error fetching account balance") 133 | } 134 | } 135 | 136 | return domain.NewAccountBalance(domain.Money(accountBSON.Balance)), nil 137 | } 138 | -------------------------------------------------------------------------------- /adapter/repository/account_postgres.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "time" 7 | 8 | "github.com/gsabadini/go-clean-architecture/domain" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type AccountSQL struct { 13 | db SQL 14 | } 15 | 16 | func NewAccountSQL(db SQL) AccountSQL { 17 | return AccountSQL{ 18 | db: db, 19 | } 20 | } 21 | 22 | func (a AccountSQL) Create(ctx context.Context, account domain.Account) (domain.Account, error) { 23 | var query = ` 24 | INSERT INTO 25 | accounts (id, name, cpf, balance, created_at) 26 | VALUES 27 | ($1, $2, $3, $4, $5) 28 | ` 29 | 30 | if err := a.db.ExecuteContext( 31 | ctx, 32 | query, 33 | account.ID(), 34 | account.Name(), 35 | account.CPF(), 36 | account.Balance(), 37 | account.CreatedAt(), 38 | ); err != nil { 39 | return domain.Account{}, errors.Wrap(err, "error creating account") 40 | } 41 | 42 | return account, nil 43 | } 44 | 45 | func (a AccountSQL) UpdateBalance(ctx context.Context, ID domain.AccountID, balance domain.Money) error { 46 | tx, ok := ctx.Value("TransactionContextKey").(Tx) 47 | if !ok { 48 | var err error 49 | tx, err = a.db.BeginTx(ctx) 50 | if err != nil { 51 | return errors.Wrap(err, "error updating account balance") 52 | } 53 | } 54 | 55 | query := "UPDATE accounts SET balance = $1 WHERE id = $2" 56 | 57 | if err := tx.ExecuteContext(ctx, query, balance, ID); err != nil { 58 | return errors.Wrap(err, "error updating account balance") 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (a AccountSQL) FindAll(ctx context.Context) ([]domain.Account, error) { 65 | var query = "SELECT * FROM accounts" 66 | 67 | rows, err := a.db.QueryContext(ctx, query) 68 | if err != nil { 69 | return []domain.Account{}, errors.Wrap(err, "error listing accounts") 70 | } 71 | 72 | var accounts = make([]domain.Account, 0) 73 | for rows.Next() { 74 | var ( 75 | ID string 76 | name string 77 | CPF string 78 | balance int64 79 | createdAt time.Time 80 | ) 81 | 82 | if err = rows.Scan(&ID, &name, &CPF, &balance, &createdAt); err != nil { 83 | return []domain.Account{}, errors.Wrap(err, "error listing accounts") 84 | } 85 | 86 | accounts = append(accounts, domain.NewAccount( 87 | domain.AccountID(ID), 88 | name, 89 | CPF, 90 | domain.Money(balance), 91 | createdAt, 92 | )) 93 | } 94 | defer rows.Close() 95 | 96 | if err = rows.Err(); err != nil { 97 | return []domain.Account{}, err 98 | } 99 | 100 | return accounts, nil 101 | } 102 | 103 | func (a AccountSQL) FindByID(ctx context.Context, ID domain.AccountID) (domain.Account, error) { 104 | tx, ok := ctx.Value("TransactionContextKey").(Tx) 105 | if !ok { 106 | var err error 107 | tx, err = a.db.BeginTx(ctx) 108 | if err != nil { 109 | return domain.Account{}, errors.Wrap(err, "error find account by id") 110 | } 111 | } 112 | 113 | var ( 114 | query = "SELECT * FROM accounts WHERE id = $1 LIMIT 1 FOR NO KEY UPDATE" 115 | id string 116 | name string 117 | CPF string 118 | balance int64 119 | createdAt time.Time 120 | ) 121 | 122 | err := tx.QueryRowContext(ctx, query, ID).Scan(&id, &name, &CPF, &balance, &createdAt) 123 | switch { 124 | case err == sql.ErrNoRows: 125 | return domain.Account{}, domain.ErrAccountNotFound 126 | default: 127 | return domain.NewAccount( 128 | domain.AccountID(id), 129 | name, 130 | CPF, 131 | domain.Money(balance), 132 | createdAt, 133 | ), err 134 | } 135 | } 136 | 137 | func (a AccountSQL) FindBalance(ctx context.Context, ID domain.AccountID) (domain.Account, error) { 138 | var ( 139 | query = "SELECT balance FROM accounts WHERE id = $1" 140 | balance int64 141 | ) 142 | 143 | err := a.db.QueryRowContext(ctx, query, ID).Scan(&balance) 144 | switch { 145 | case err == sql.ErrNoRows: 146 | return domain.Account{}, domain.ErrAccountNotFound 147 | default: 148 | return domain.NewAccountBalance(domain.Money(balance)), err 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /adapter/repository/nosql.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "context" 4 | 5 | type NoSQL interface { 6 | Store(context.Context, string, interface{}) error 7 | Update(context.Context, string, interface{}, interface{}) error 8 | FindAll(context.Context, string, interface{}, interface{}) error 9 | FindOne(context.Context, string, interface{}, interface{}, interface{}) error 10 | StartSession() (Session, error) 11 | } 12 | 13 | type Session interface { 14 | WithTransaction(context.Context, func(context.Context) error) error 15 | EndSession(context.Context) 16 | } 17 | -------------------------------------------------------------------------------- /adapter/repository/sql.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "context" 4 | 5 | type SQL interface { 6 | ExecuteContext(context.Context, string, ...interface{}) error 7 | QueryContext(context.Context, string, ...interface{}) (Rows, error) 8 | QueryRowContext(context.Context, string, ...interface{}) Row 9 | BeginTx(ctx context.Context) (Tx, error) 10 | } 11 | 12 | type Rows interface { 13 | Scan(dest ...interface{}) error 14 | Next() bool 15 | Err() error 16 | Close() error 17 | } 18 | 19 | type Row interface { 20 | Scan(dest ...interface{}) error 21 | } 22 | 23 | type Tx interface { 24 | ExecuteContext(context.Context, string, ...interface{}) error 25 | QueryContext(context.Context, string, ...interface{}) (Rows, error) 26 | QueryRowContext(context.Context, string, ...interface{}) Row 27 | Commit() error 28 | Rollback() error 29 | } 30 | -------------------------------------------------------------------------------- /adapter/repository/transfer_mongodb.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | 9 | "github.com/pkg/errors" 10 | "go.mongodb.org/mongo-driver/bson" 11 | ) 12 | 13 | type transferBSON struct { 14 | ID string `bson:"id"` 15 | AccountOriginID string `bson:"account_origin_id"` 16 | AccountDestinationID string `bson:"account_destination_id"` 17 | Amount int64 `bson:"amount"` 18 | CreatedAt time.Time `bson:"created_at"` 19 | } 20 | 21 | type TransferNoSQL struct { 22 | collectionName string 23 | db NoSQL 24 | } 25 | 26 | func NewTransferNoSQL(db NoSQL) TransferNoSQL { 27 | return TransferNoSQL{ 28 | db: db, 29 | collectionName: "transfers", 30 | } 31 | } 32 | 33 | func (t TransferNoSQL) Create(ctx context.Context, transfer domain.Transfer) (domain.Transfer, error) { 34 | transferBSON := &transferBSON{ 35 | ID: transfer.ID().String(), 36 | AccountOriginID: transfer.AccountOriginID().String(), 37 | AccountDestinationID: transfer.AccountDestinationID().String(), 38 | Amount: transfer.Amount().Int64(), 39 | CreatedAt: transfer.CreatedAt(), 40 | } 41 | 42 | if err := t.db.Store(ctx, t.collectionName, transferBSON); err != nil { 43 | return domain.Transfer{}, errors.Wrap(err, "error creating transfer") 44 | } 45 | 46 | return transfer, nil 47 | } 48 | 49 | func (t TransferNoSQL) FindAll(ctx context.Context) ([]domain.Transfer, error) { 50 | var transfersBSON = make([]transferBSON, 0) 51 | 52 | if err := t.db.FindAll(ctx, t.collectionName, bson.M{}, &transfersBSON); err != nil { 53 | return []domain.Transfer{}, errors.Wrap(err, "error listing transfers") 54 | } 55 | 56 | var transfers = make([]domain.Transfer, 0) 57 | 58 | for _, transferBSON := range transfersBSON { 59 | var transfer = domain.NewTransfer( 60 | domain.TransferID(transferBSON.ID), 61 | domain.AccountID(transferBSON.AccountOriginID), 62 | domain.AccountID(transferBSON.AccountDestinationID), 63 | domain.Money(transferBSON.Amount), 64 | transferBSON.CreatedAt, 65 | ) 66 | 67 | transfers = append(transfers, transfer) 68 | } 69 | 70 | return transfers, nil 71 | } 72 | 73 | func (t TransferNoSQL) WithTransaction(ctx context.Context, fn func(ctxTx context.Context) error) error { 74 | session, err := t.db.StartSession() 75 | if err != nil { 76 | return err 77 | } 78 | defer session.EndSession(ctx) 79 | 80 | err = session.WithTransaction(ctx, fn) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /adapter/repository/transfer_postgres.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type TransferSQL struct { 12 | db SQL 13 | } 14 | 15 | func NewTransferSQL(db SQL) TransferSQL { 16 | return TransferSQL{ 17 | db: db, 18 | } 19 | } 20 | 21 | func (t TransferSQL) Create(ctx context.Context, transfer domain.Transfer) (domain.Transfer, error) { 22 | tx, ok := ctx.Value("TransactionContextKey").(Tx) 23 | if !ok { 24 | var err error 25 | tx, err = t.db.BeginTx(ctx) 26 | if err != nil { 27 | return domain.Transfer{}, errors.Wrap(err, "error creating transfer") 28 | } 29 | } 30 | 31 | var query = ` 32 | INSERT INTO 33 | transfers (id, account_origin_id, account_destination_id, amount, created_at) 34 | VALUES 35 | ($1, $2, $3, $4, $5) 36 | ` 37 | 38 | if err := tx.ExecuteContext( 39 | ctx, 40 | query, 41 | transfer.ID(), 42 | transfer.AccountOriginID(), 43 | transfer.AccountDestinationID(), 44 | transfer.Amount(), 45 | transfer.CreatedAt(), 46 | ); err != nil { 47 | return domain.Transfer{}, errors.Wrap(err, "error creating transfer") 48 | } 49 | 50 | return transfer, nil 51 | } 52 | 53 | func (t TransferSQL) FindAll(ctx context.Context) ([]domain.Transfer, error) { 54 | var query = "SELECT * FROM transfers" 55 | 56 | rows, err := t.db.QueryContext(ctx, query) 57 | if err != nil { 58 | return []domain.Transfer{}, errors.Wrap(err, "error listing transfers") 59 | } 60 | 61 | var transfers = make([]domain.Transfer, 0) 62 | for rows.Next() { 63 | var ( 64 | ID string 65 | accountOriginID string 66 | accountDestinationID string 67 | amount int64 68 | createdAt time.Time 69 | ) 70 | 71 | if err = rows.Scan(&ID, &accountOriginID, &accountDestinationID, &amount, &createdAt); err != nil { 72 | return []domain.Transfer{}, errors.Wrap(err, "error listing transfers") 73 | } 74 | 75 | transfers = append(transfers, domain.NewTransfer( 76 | domain.TransferID(ID), 77 | domain.AccountID(accountOriginID), 78 | domain.AccountID(accountDestinationID), 79 | domain.Money(amount), 80 | createdAt, 81 | )) 82 | } 83 | defer rows.Close() 84 | 85 | if err = rows.Err(); err != nil { 86 | return []domain.Transfer{}, err 87 | } 88 | 89 | return transfers, nil 90 | } 91 | 92 | func (t TransferSQL) WithTransaction(ctx context.Context, fn func(ctxTx context.Context) error) error { 93 | tx, err := t.db.BeginTx(ctx) 94 | if err != nil { 95 | return errors.Wrap(err, "error begin tx") 96 | } 97 | 98 | ctxTx := context.WithValue(ctx, "TransactionContextKey", tx) 99 | err = fn(ctxTx) 100 | if err != nil { 101 | if rbErr := tx.Rollback(); rbErr != nil { 102 | return errors.Wrap(err, "rollback error") 103 | } 104 | return err 105 | } 106 | 107 | return tx.Commit() 108 | } 109 | -------------------------------------------------------------------------------- /adapter/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | type Validator interface { 4 | Validate(interface{}) error 5 | Messages() []string 6 | } 7 | -------------------------------------------------------------------------------- /clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSabadini/go-clean-architecture/d88092281c40d8e0305bc1c8ce9019909e41fada/clean.png -------------------------------------------------------------------------------- /create_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSabadini/go-clean-architecture/d88092281c40d8e0305bc1c8ce9019909e41fada/create_account.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | app: 5 | container_name: "go-clean-architecture" 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | target: development 10 | ports: 11 | - "3001:3001" 12 | volumes: 13 | - ./:/app 14 | - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache 15 | env_file: 16 | - .env 17 | networks: 18 | - main 19 | depends_on: 20 | - mongodb-primary 21 | - postgres 22 | 23 | postgres: 24 | container_name: "postgres" 25 | image: "postgres:12.2-alpine" 26 | ports: 27 | - "5432:5432" 28 | environment: 29 | POSTGRES_USER: dev 30 | POSTGRES_PASSWORD: dev 31 | POSTGRES_DB: bank 32 | volumes: 33 | - ./_scripts/postgres:/docker-entrypoint-initdb.d 34 | networks: 35 | - main 36 | 37 | mongodb-primary: 38 | container_name: mongodb-primary 39 | image: 'docker.io/bitnami/mongodb:4.4-debian-10' 40 | environment: 41 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-primary 42 | - MONGODB_REPLICA_SET_MODE=primary 43 | - MONGODB_ROOT_PASSWORD=password123 44 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 45 | ports: 46 | - "27017:27017" 47 | networks: 48 | - main 49 | 50 | mongodb-secondary: 51 | container_name: mongodb-secondary 52 | image: 'docker.io/bitnami/mongodb:4.4-debian-10' 53 | depends_on: 54 | - mongodb-primary 55 | environment: 56 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-secondary 57 | - MONGODB_REPLICA_SET_MODE=secondary 58 | - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary 59 | - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=password123 60 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 61 | networks: 62 | - main 63 | 64 | mongodb-arbiter: 65 | container_name: mongodb-arbiter 66 | image: 'docker.io/bitnami/mongodb:4.4-debian-10' 67 | depends_on: 68 | - mongodb-primary 69 | environment: 70 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-arbiter 71 | - MONGODB_REPLICA_SET_MODE=arbiter 72 | - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary 73 | - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=password123 74 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 75 | networks: 76 | - main 77 | 78 | networks: 79 | main: 80 | -------------------------------------------------------------------------------- /domain/account.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | ErrAccountNotFound = errors.New("account not found") 11 | 12 | ErrAccountOriginNotFound = errors.New("account origin not found") 13 | 14 | ErrAccountDestinationNotFound = errors.New("account destination not found") 15 | 16 | ErrInsufficientBalance = errors.New("origin account does not have sufficient balance") 17 | ) 18 | 19 | type AccountID string 20 | 21 | func (a AccountID) String() string { 22 | return string(a) 23 | } 24 | 25 | type ( 26 | AccountRepository interface { 27 | Create(context.Context, Account) (Account, error) 28 | UpdateBalance(context.Context, AccountID, Money) error 29 | FindAll(context.Context) ([]Account, error) 30 | FindByID(context.Context, AccountID) (Account, error) 31 | FindBalance(context.Context, AccountID) (Account, error) 32 | } 33 | 34 | Account struct { 35 | id AccountID 36 | name string 37 | cpf string 38 | balance Money 39 | createdAt time.Time 40 | } 41 | ) 42 | 43 | func NewAccount(ID AccountID, name, CPF string, balance Money, createdAt time.Time) Account { 44 | return Account{ 45 | id: ID, 46 | name: name, 47 | cpf: CPF, 48 | balance: balance, 49 | createdAt: createdAt, 50 | } 51 | } 52 | 53 | func (a *Account) Deposit(amount Money) { 54 | a.balance += amount 55 | } 56 | 57 | func (a *Account) Withdraw(amount Money) error { 58 | if a.balance < amount { 59 | return ErrInsufficientBalance 60 | } 61 | 62 | a.balance -= amount 63 | 64 | return nil 65 | } 66 | 67 | func (a Account) ID() AccountID { 68 | return a.id 69 | } 70 | 71 | func (a Account) Name() string { 72 | return a.name 73 | } 74 | 75 | func (a Account) CPF() string { 76 | return a.cpf 77 | } 78 | 79 | func (a Account) Balance() Money { 80 | return a.balance 81 | } 82 | 83 | func (a Account) CreatedAt() time.Time { 84 | return a.createdAt 85 | } 86 | 87 | func NewAccountBalance(balance Money) Account { 88 | return Account{balance: balance} 89 | } 90 | -------------------------------------------------------------------------------- /domain/account_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAccount_Deposit(t *testing.T) { 8 | t.Parallel() 9 | 10 | type args struct { 11 | amount Money 12 | } 13 | 14 | tests := []struct { 15 | name string 16 | account Account 17 | args args 18 | expected Money 19 | }{ 20 | { 21 | name: "Successful depositing balance", 22 | args: args{ 23 | amount: 10, 24 | }, 25 | account: NewAccountBalance(0), 26 | expected: 10, 27 | }, 28 | { 29 | name: "Successful depositing balance", 30 | args: args{ 31 | amount: 102098, 32 | }, 33 | account: NewAccountBalance(0), 34 | expected: 102098, 35 | }, 36 | { 37 | name: "Successful depositing balance", 38 | args: args{ 39 | amount: 4498, 40 | }, 41 | account: NewAccountBalance(98), 42 | expected: 4596, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | tt.account.Deposit(tt.args.amount) 49 | 50 | if tt.account.Balance() != tt.expected { 51 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", 52 | tt.name, 53 | tt.account.Balance(), 54 | tt.expected, 55 | ) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestAccount_Withdraw(t *testing.T) { 62 | t.Parallel() 63 | 64 | type args struct { 65 | amount Money 66 | } 67 | 68 | tests := []struct { 69 | name string 70 | account Account 71 | args args 72 | expected Money 73 | expectedErr error 74 | }{ 75 | { 76 | name: "Success in withdrawing balance", 77 | args: args{ 78 | amount: 10, 79 | }, 80 | account: NewAccountBalance(10), 81 | expected: 0, 82 | }, 83 | { 84 | name: "Success in withdrawing balance", 85 | args: args{ 86 | amount: 10012, 87 | }, 88 | account: NewAccountBalance(10013), 89 | expected: 1, 90 | }, 91 | { 92 | name: "Success in withdrawing balance", 93 | args: args{ 94 | amount: 25, 95 | }, 96 | account: NewAccountBalance(125), 97 | expected: 100, 98 | }, 99 | { 100 | name: "error when withdrawing account balance without sufficient balance", 101 | args: args{ 102 | amount: 564, 103 | }, 104 | account: NewAccountBalance(62), 105 | expectedErr: ErrInsufficientBalance, 106 | }, 107 | { 108 | name: "error when withdrawing account balance without sufficient balance", 109 | args: args{ 110 | amount: 5, 111 | }, 112 | account: NewAccountBalance(1), 113 | expectedErr: ErrInsufficientBalance, 114 | }, 115 | { 116 | name: "error when withdrawing account balance without sufficient balance", 117 | args: args{ 118 | amount: 10, 119 | }, 120 | account: NewAccountBalance(0), 121 | expectedErr: ErrInsufficientBalance, 122 | }, 123 | } 124 | 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | var err error 128 | if err = tt.account.Withdraw(tt.args.amount); (err != nil) && (err.Error() != tt.expectedErr.Error()) { 129 | t.Errorf("[TestCase '%s'] ResultError: '%v' | ExpectedError: '%v'", 130 | tt.name, 131 | err, 132 | tt.expectedErr.Error(), 133 | ) 134 | return 135 | } 136 | 137 | if tt.expectedErr == nil && tt.account.Balance() != tt.expected { 138 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", 139 | tt.name, 140 | tt.account.Balance(), 141 | tt.expected, 142 | ) 143 | } 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /domain/money.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Money int64 4 | 5 | func (m Money) Float64() float64 { 6 | return float64(m) / 100 7 | } 8 | 9 | func (m Money) Int64() int64 { 10 | return int64(m) 11 | } 12 | -------------------------------------------------------------------------------- /domain/transfer.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type TransferID string 9 | 10 | func (t TransferID) String() string { 11 | return string(t) 12 | } 13 | 14 | type ( 15 | TransferRepository interface { 16 | Create(context.Context, Transfer) (Transfer, error) 17 | FindAll(context.Context) ([]Transfer, error) 18 | WithTransaction(context.Context, func(context.Context) error) error 19 | } 20 | 21 | Transfer struct { 22 | id TransferID 23 | accountOriginID AccountID 24 | accountDestinationID AccountID 25 | amount Money 26 | createdAt time.Time 27 | } 28 | ) 29 | 30 | func NewTransfer( 31 | ID TransferID, 32 | accountOriginID AccountID, 33 | accountDestinationID AccountID, 34 | amount Money, 35 | createdAt time.Time, 36 | ) Transfer { 37 | return Transfer{ 38 | id: ID, 39 | accountOriginID: accountOriginID, 40 | accountDestinationID: accountDestinationID, 41 | amount: amount, 42 | createdAt: createdAt, 43 | } 44 | } 45 | 46 | func (t Transfer) ID() TransferID { 47 | return t.id 48 | } 49 | 50 | func (t Transfer) AccountOriginID() AccountID { 51 | return t.accountOriginID 52 | } 53 | 54 | func (t Transfer) AccountDestinationID() AccountID { 55 | return t.accountDestinationID 56 | } 57 | 58 | func (t Transfer) Amount() Money { 59 | return t.amount 60 | } 61 | 62 | func (t Transfer) CreatedAt() time.Time { 63 | return t.createdAt 64 | } 65 | -------------------------------------------------------------------------------- /domain/uuid.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | gouuid "github.com/satori/go.uuid" 5 | ) 6 | 7 | func NewUUID() string { 8 | return gouuid.NewV4().String() 9 | } 10 | 11 | func IsValidUUID(uuid string) bool { 12 | _, err := gouuid.FromString(uuid) 13 | return err == nil 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gsabadini/go-clean-architecture 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/go-playground/locales v0.14.1 8 | github.com/go-playground/universal-translator v0.18.1 9 | github.com/go-playground/validator/v10 v10.16.0 10 | github.com/gorilla/mux v1.8.1 11 | github.com/lib/pq v1.10.9 12 | github.com/pkg/errors v0.9.1 13 | github.com/satori/go.uuid v1.2.0 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/urfave/negroni v1.0.0 16 | go.mongodb.org/mongo-driver v1.13.0 17 | go.uber.org/zap v1.26.0 18 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 19 | ) 20 | 21 | require ( 22 | github.com/bytedance/sonic v1.10.2 // indirect 23 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 24 | github.com/chenzhuoyu/iasm v0.9.1 // indirect 25 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 26 | github.com/gin-contrib/sse v0.1.0 // indirect 27 | github.com/goccy/go-json v0.10.2 // indirect 28 | github.com/golang/snappy v0.0.1 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/klauspost/compress v1.13.6 // indirect 31 | github.com/klauspost/cpuid/v2 v2.2.6 // indirect 32 | github.com/leodido/go-urn v1.2.4 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect 37 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 38 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 39 | github.com/ugorji/go/codec v1.2.11 // indirect 40 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 41 | github.com/xdg-go/scram v1.1.2 // indirect 42 | github.com/xdg-go/stringprep v1.0.4 // indirect 43 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 44 | go.uber.org/multierr v1.11.0 // indirect 45 | golang.org/x/arch v0.6.0 // indirect 46 | golang.org/x/crypto v0.15.0 // indirect 47 | golang.org/x/net v0.18.0 // indirect 48 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect 49 | golang.org/x/sys v0.14.0 // indirect 50 | golang.org/x/text v0.14.0 // indirect 51 | google.golang.org/protobuf v1.31.0 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 3 | github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= 4 | github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 5 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 8 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 9 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 10 | github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= 11 | github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 16 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 17 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 18 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 19 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 20 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= 28 | github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 29 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 30 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 31 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 32 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 33 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 34 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 36 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 39 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 42 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 43 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 44 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 45 | github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= 46 | github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 47 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 48 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 49 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 50 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 51 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 52 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 53 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 54 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 58 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 59 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= 60 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 61 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 62 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 68 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 69 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 70 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 71 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 72 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 73 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 74 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 75 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 78 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 79 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 80 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 81 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 82 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 83 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 84 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 85 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 86 | github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= 87 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 88 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 89 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 90 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 91 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 92 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 93 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 94 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 95 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 96 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 97 | go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY= 98 | go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= 99 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 100 | go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 101 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 102 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 103 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 104 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 105 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 106 | golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= 107 | golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 108 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 109 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 110 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 111 | golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= 112 | golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 113 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 114 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 115 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 116 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 117 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 118 | golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= 119 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 120 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= 122 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 124 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 133 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 134 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 135 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 136 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 137 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 138 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 139 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 140 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 141 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 142 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 143 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 144 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 145 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 146 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 147 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 148 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 149 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 151 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 152 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 153 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 154 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 155 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= 156 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 157 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 158 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 159 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 160 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 161 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 162 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 163 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 164 | -------------------------------------------------------------------------------- /infrastructure/database/config.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "os" 5 | "time" 6 | ) 7 | 8 | type config struct { 9 | host string 10 | database string 11 | port string 12 | driver string 13 | user string 14 | password string 15 | 16 | ctxTimeout time.Duration 17 | } 18 | 19 | func newConfigMongoDB() *config { 20 | return &config{ 21 | host: os.Getenv("MONGODB_HOST"), 22 | database: os.Getenv("MONGODB_HOST"), 23 | password: os.Getenv("MONGODB_ROOT_PASSWORD"), 24 | user: os.Getenv("MONGODB_ROOT_USER"), 25 | ctxTimeout: 60 * time.Second, 26 | } 27 | } 28 | 29 | func newConfigPostgres() *config { 30 | return &config{ 31 | host: os.Getenv("POSTGRES_HOST"), 32 | database: os.Getenv("POSTGRES_DATABASE"), 33 | port: os.Getenv("POSTGRES_PORT"), 34 | driver: os.Getenv("POSTGRES_DRIVER"), 35 | user: os.Getenv("POSTGRES_USER"), 36 | password: os.Getenv("POSTGRES_PASSWORD"), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /infrastructure/database/factory_nosql.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 7 | ) 8 | 9 | var ( 10 | errInvalidNoSQLDatabaseInstance = errors.New("invalid nosql db instance") 11 | ) 12 | 13 | const ( 14 | InstanceMongoDB int = iota 15 | ) 16 | 17 | func NewDatabaseNoSQLFactory(instance int) (repository.NoSQL, error) { 18 | switch instance { 19 | case InstanceMongoDB: 20 | return NewMongoHandler(newConfigMongoDB()) 21 | default: 22 | return nil, errInvalidNoSQLDatabaseInstance 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /infrastructure/database/factory_sql.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 7 | ) 8 | 9 | var ( 10 | errInvalidSQLDatabaseInstance = errors.New("invalid sql db instance") 11 | ) 12 | 13 | const ( 14 | InstancePostgres int = iota 15 | ) 16 | 17 | func NewDatabaseSQLFactory(instance int) (repository.SQL, error) { 18 | switch instance { 19 | case InstancePostgres: 20 | return NewPostgresHandler(newConfigPostgres()) 21 | default: 22 | return nil, errInvalidSQLDatabaseInstance 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /infrastructure/database/mongo_handler.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 7 | "log" 8 | 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | ) 12 | 13 | type mongoHandler struct { 14 | db *mongo.Database 15 | client *mongo.Client 16 | } 17 | 18 | func NewMongoHandler(c *config) (*mongoHandler, error) { 19 | ctx, cancel := context.WithTimeout(context.Background(), c.ctxTimeout) 20 | defer cancel() 21 | 22 | uri := fmt.Sprintf( 23 | "%s://%s:%s@mongodb-primary,mongodb-secondary,mongodb-arbiter/?replicaSet=replicaset", 24 | c.host, 25 | c.user, 26 | c.password, 27 | ) 28 | 29 | clientOpts := options.Client().ApplyURI(uri) 30 | client, err := mongo.Connect(ctx, clientOpts) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | err = client.Ping(ctx, nil) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | return &mongoHandler{ 41 | db: client.Database(c.database), 42 | client: client, 43 | }, nil 44 | } 45 | 46 | func (mgo mongoHandler) Store(ctx context.Context, collection string, data interface{}) error { 47 | if _, err := mgo.db.Collection(collection).InsertOne(ctx, data); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (mgo mongoHandler) Update(ctx context.Context, collection string, query interface{}, update interface{}) error { 55 | if _, err := mgo.db.Collection(collection).UpdateOne(ctx, query, update); err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (mgo mongoHandler) FindAll(ctx context.Context, collection string, query interface{}, result interface{}) error { 63 | cur, err := mgo.db.Collection(collection).Find(ctx, query) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | defer cur.Close(ctx) 69 | if err = cur.All(ctx, result); err != nil { 70 | return err 71 | } 72 | 73 | if err := cur.Err(); err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (mgo mongoHandler) FindOne( 81 | ctx context.Context, 82 | collection string, 83 | query interface{}, 84 | projection interface{}, 85 | result interface{}, 86 | ) error { 87 | var err = mgo.db.Collection(collection). 88 | FindOne( 89 | ctx, 90 | query, 91 | options.FindOne().SetProjection(projection), 92 | ).Decode(result) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (mgo *mongoHandler) StartSession() (repository.Session, error) { 101 | session, err := mgo.client.StartSession() 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | return newMongoHandlerSession(session), nil 107 | } 108 | 109 | type mongoDBSession struct { 110 | session mongo.Session 111 | } 112 | 113 | func newMongoHandlerSession(session mongo.Session) *mongoDBSession { 114 | return &mongoDBSession{session: session} 115 | } 116 | 117 | func (m *mongoDBSession) WithTransaction(ctx context.Context, fn func(context.Context) error) error { 118 | callback := func(sessCtx mongo.SessionContext) (interface{}, error) { 119 | err := fn(sessCtx) 120 | if err != nil { 121 | return nil, err 122 | } 123 | return nil, nil 124 | } 125 | 126 | _, err := m.session.WithTransaction(ctx, callback) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (m *mongoDBSession) EndSession(ctx context.Context) { 135 | m.session.EndSession(ctx) 136 | } 137 | -------------------------------------------------------------------------------- /infrastructure/database/mongo_handler_deprecated.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | mongo "gopkg.in/mgo.v2" 7 | ) 8 | 9 | type mongoHandlerDeprecated struct { 10 | database *mongo.Database 11 | session *mongo.Session 12 | } 13 | 14 | func NewMongoHandlerDeprecated(c *config) (*mongoHandlerDeprecated, error) { 15 | session, err := mongo.DialWithTimeout(c.host, c.ctxTimeout) 16 | if err != nil { 17 | return &mongoHandlerDeprecated{}, err 18 | } 19 | 20 | handler := new(mongoHandlerDeprecated) 21 | handler.session = session 22 | handler.database = handler.session.DB(c.database) 23 | 24 | return handler, nil 25 | } 26 | 27 | func (mgo mongoHandlerDeprecated) Store(_ context.Context, collection string, data interface{}) error { 28 | session := mgo.session.Clone() 29 | defer session.Close() 30 | 31 | return mgo.database.C(collection).With(session).Insert(data) 32 | } 33 | 34 | func (mgo mongoHandlerDeprecated) Update(_ context.Context, collection string, query interface{}, update interface{}) error { 35 | session := mgo.session.Clone() 36 | defer session.Close() 37 | 38 | return mgo.database.C(collection).With(session).Update(query, update) 39 | } 40 | 41 | func (mgo mongoHandlerDeprecated) FindAll(_ context.Context, collection string, query interface{}, result interface{}) error { 42 | session := mgo.session.Clone() 43 | defer session.Close() 44 | 45 | return mgo.database.C(collection).With(session).Find(query).All(result) 46 | } 47 | 48 | func (mgo mongoHandlerDeprecated) FindOne( 49 | _ context.Context, 50 | collection string, 51 | query interface{}, 52 | selector interface{}, 53 | result interface{}, 54 | ) error { 55 | session := mgo.session.Clone() 56 | defer session.Close() 57 | 58 | return mgo.database.C(collection).With(session).Find(query).Select(selector).One(result) 59 | } 60 | -------------------------------------------------------------------------------- /infrastructure/database/postgres_handler.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 10 | 11 | _ "github.com/lib/pq" 12 | ) 13 | 14 | type postgresHandler struct { 15 | db *sql.DB 16 | } 17 | 18 | func NewPostgresHandler(c *config) (*postgresHandler, error) { 19 | var ds = fmt.Sprintf( 20 | "host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", 21 | c.host, 22 | c.port, 23 | c.user, 24 | c.database, 25 | c.password, 26 | ) 27 | 28 | fmt.Println(ds) 29 | db, err := sql.Open(c.driver, ds) 30 | if err != nil { 31 | return &postgresHandler{}, err 32 | } 33 | 34 | err = db.Ping() 35 | if err != nil { 36 | log.Fatalln(err) 37 | } 38 | 39 | return &postgresHandler{db: db}, nil 40 | } 41 | 42 | func (p postgresHandler) BeginTx(ctx context.Context) (repository.Tx, error) { 43 | tx, err := p.db.BeginTx(ctx, &sql.TxOptions{}) 44 | if err != nil { 45 | return postgresTx{}, err 46 | } 47 | 48 | return newPostgresTx(tx), nil 49 | } 50 | 51 | func (p postgresHandler) ExecuteContext(ctx context.Context, query string, args ...interface{}) error { 52 | _, err := p.db.ExecContext(ctx, query, args...) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (p postgresHandler) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) { 61 | rows, err := p.db.QueryContext(ctx, query, args...) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | row := newPostgresRows(rows) 67 | 68 | return row, nil 69 | } 70 | 71 | func (p postgresHandler) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row { 72 | row := p.db.QueryRowContext(ctx, query, args...) 73 | 74 | return newPostgresRow(row) 75 | } 76 | 77 | type postgresRow struct { 78 | row *sql.Row 79 | } 80 | 81 | func newPostgresRow(row *sql.Row) postgresRow { 82 | return postgresRow{row: row} 83 | } 84 | 85 | func (pr postgresRow) Scan(dest ...interface{}) error { 86 | if err := pr.row.Scan(dest...); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | type postgresRows struct { 94 | rows *sql.Rows 95 | } 96 | 97 | func newPostgresRows(rows *sql.Rows) postgresRows { 98 | return postgresRows{rows: rows} 99 | } 100 | 101 | func (pr postgresRows) Scan(dest ...interface{}) error { 102 | if err := pr.rows.Scan(dest...); err != nil { 103 | return err 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (pr postgresRows) Next() bool { 110 | return pr.rows.Next() 111 | } 112 | 113 | func (pr postgresRows) Err() error { 114 | return pr.rows.Err() 115 | } 116 | 117 | func (pr postgresRows) Close() error { 118 | return pr.rows.Close() 119 | } 120 | 121 | type postgresTx struct { 122 | tx *sql.Tx 123 | } 124 | 125 | func newPostgresTx(tx *sql.Tx) postgresTx { 126 | return postgresTx{tx: tx} 127 | } 128 | 129 | func (p postgresTx) ExecuteContext(ctx context.Context, query string, args ...interface{}) error { 130 | _, err := p.tx.ExecContext(ctx, query, args...) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (p postgresTx) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) { 139 | rows, err := p.tx.QueryContext(ctx, query, args...) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | row := newPostgresRows(rows) 145 | 146 | return row, nil 147 | } 148 | 149 | func (p postgresTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row { 150 | row := p.tx.QueryRowContext(ctx, query, args...) 151 | 152 | return newPostgresRow(row) 153 | } 154 | 155 | func (p postgresTx) Commit() error { 156 | return p.tx.Commit() 157 | } 158 | 159 | func (p postgresTx) Rollback() error { 160 | return p.tx.Rollback() 161 | } 162 | -------------------------------------------------------------------------------- /infrastructure/http_server.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 8 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 9 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 10 | "github.com/gsabadini/go-clean-architecture/infrastructure/database" 11 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 12 | "github.com/gsabadini/go-clean-architecture/infrastructure/router" 13 | "github.com/gsabadini/go-clean-architecture/infrastructure/validation" 14 | ) 15 | 16 | type config struct { 17 | appName string 18 | logger logger.Logger 19 | validator validator.Validator 20 | dbSQL repository.SQL 21 | dbNoSQL repository.NoSQL 22 | ctxTimeout time.Duration 23 | webServerPort router.Port 24 | webServer router.Server 25 | } 26 | 27 | func NewConfig() *config { 28 | return &config{} 29 | } 30 | 31 | func (c *config) ContextTimeout(t time.Duration) *config { 32 | c.ctxTimeout = t 33 | return c 34 | } 35 | 36 | func (c *config) Name(name string) *config { 37 | c.appName = name 38 | return c 39 | } 40 | 41 | func (c *config) Logger(instance int) *config { 42 | log, err := log.NewLoggerFactory(instance) 43 | if err != nil { 44 | log.Fatalln(err) 45 | } 46 | 47 | c.logger = log 48 | c.logger.Infof("Successfully configured log") 49 | return c 50 | } 51 | 52 | func (c *config) DbSQL(instance int) *config { 53 | db, err := database.NewDatabaseSQLFactory(instance) 54 | if err != nil { 55 | c.logger.Fatalln(err, "Could not make a connection to the database") 56 | } 57 | 58 | c.logger.Infof("Successfully connected to the SQL database") 59 | 60 | c.dbSQL = db 61 | return c 62 | } 63 | 64 | func (c *config) DbNoSQL(instance int) *config { 65 | db, err := database.NewDatabaseNoSQLFactory(instance) 66 | if err != nil { 67 | c.logger.Fatalln(err, "Could not make a connection to the database") 68 | } 69 | 70 | c.logger.Infof("Successfully connected to the NoSQL database") 71 | 72 | c.dbNoSQL = db 73 | return c 74 | } 75 | 76 | func (c *config) Validator(instance int) *config { 77 | v, err := validation.NewValidatorFactory(instance) 78 | if err != nil { 79 | c.logger.Fatalln(err) 80 | } 81 | 82 | c.logger.Infof("Successfully configured validator") 83 | 84 | c.validator = v 85 | return c 86 | } 87 | 88 | func (c *config) WebServer(instance int) *config { 89 | s, err := router.NewWebServerFactory( 90 | instance, 91 | c.logger, 92 | c.dbSQL, 93 | c.dbNoSQL, 94 | c.validator, 95 | c.webServerPort, 96 | c.ctxTimeout, 97 | ) 98 | 99 | if err != nil { 100 | c.logger.Fatalln(err) 101 | } 102 | 103 | c.logger.Infof("Successfully configured router server") 104 | 105 | c.webServer = s 106 | return c 107 | } 108 | 109 | func (c *config) WebServerPort(port string) *config { 110 | p, err := strconv.ParseInt(port, 10, 64) 111 | if err != nil { 112 | c.logger.Fatalln(err) 113 | } 114 | 115 | c.webServerPort = router.Port(p) 116 | return c 117 | } 118 | 119 | func (c *config) Start() { 120 | c.webServer.Listen() 121 | } 122 | -------------------------------------------------------------------------------- /infrastructure/log/factory.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 7 | ) 8 | 9 | const ( 10 | InstanceZapLogger int = iota 11 | InstanceLogrusLogger 12 | ) 13 | 14 | var ( 15 | errInvalidLoggerInstance = errors.New("invalid log instance") 16 | ) 17 | 18 | func NewLoggerFactory(instance int) (logger.Logger, error) { 19 | switch instance { 20 | case InstanceZapLogger: 21 | return NewZapLogger() 22 | case InstanceLogrusLogger: 23 | return NewLogrusLogger(), nil 24 | default: 25 | return nil, errInvalidLoggerInstance 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /infrastructure/log/logger_mock.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/gsabadini/go-clean-architecture/adapter/logger" 4 | 5 | type LoggerMock struct{} 6 | 7 | func (l LoggerMock) Infof(_ string, _ ...interface{}) {} 8 | func (l LoggerMock) Warnf(_ string, _ ...interface{}) {} 9 | func (l LoggerMock) Errorf(_ string, _ ...interface{}) {} 10 | func (l LoggerMock) Fatalln(_ ...interface{}) {} 11 | func (l LoggerMock) WithFields(_ logger.Fields) logger.Logger { return LoggerEntryMock{} } 12 | func (l LoggerMock) WithError(_ error) logger.Logger { return LoggerEntryMock{} } 13 | 14 | type LoggerEntryMock struct{} 15 | 16 | func (l LoggerEntryMock) Infof(_ string, _ ...interface{}) {} 17 | func (l LoggerEntryMock) Warnf(_ string, _ ...interface{}) {} 18 | func (l LoggerEntryMock) Errorf(_ string, _ ...interface{}) {} 19 | func (l LoggerEntryMock) Fatalln(_ ...interface{}) {} 20 | func (l LoggerEntryMock) WithFields(_ logger.Fields) logger.Logger { return LoggerEntryMock{} } 21 | func (l LoggerEntryMock) WithError(_ error) logger.Logger { return LoggerEntryMock{} } 22 | -------------------------------------------------------------------------------- /infrastructure/log/logrus.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | type logrusLogger struct { 9 | logger *logrus.Logger 10 | } 11 | 12 | func NewLogrusLogger() logger.Logger { 13 | log := logrus.New() 14 | log.SetFormatter(&logrus.JSONFormatter{ 15 | TimestampFormat: "2006-01-02 15:04:05", 16 | }) 17 | 18 | return &logrusLogger{logger: log} 19 | } 20 | 21 | func (l *logrusLogger) Infof(format string, args ...interface{}) { 22 | l.logger.Infof(format, args...) 23 | } 24 | 25 | func (l *logrusLogger) Warnf(format string, args ...interface{}) { 26 | l.logger.Warnf(format, args...) 27 | } 28 | 29 | func (l *logrusLogger) Errorf(format string, args ...interface{}) { 30 | l.logger.Errorf(format, args...) 31 | } 32 | 33 | func (l *logrusLogger) Fatalln(args ...interface{}) { 34 | l.logger.Fatalln(args...) 35 | } 36 | 37 | func (l *logrusLogger) WithFields(fields logger.Fields) logger.Logger { 38 | return &logrusLogEntry{ 39 | entry: l.logger.WithFields(convertToLogrusFields(fields)), 40 | } 41 | } 42 | 43 | func (l *logrusLogger) WithError(err error) logger.Logger { 44 | return &logrusLogEntry{ 45 | entry: l.logger.WithError(err), 46 | } 47 | } 48 | 49 | type logrusLogEntry struct { 50 | entry *logrus.Entry 51 | } 52 | 53 | func (l *logrusLogEntry) Infof(format string, args ...interface{}) { 54 | l.entry.Infof(format, args...) 55 | } 56 | 57 | func (l *logrusLogEntry) Warnf(format string, args ...interface{}) { 58 | l.entry.Warnf(format, args...) 59 | } 60 | 61 | func (l *logrusLogEntry) Errorf(format string, args ...interface{}) { 62 | l.entry.Errorf(format, args...) 63 | } 64 | 65 | func (l *logrusLogEntry) Fatalln(args ...interface{}) { 66 | l.entry.Fatalln(args...) 67 | } 68 | 69 | func (l *logrusLogEntry) WithFields(fields logger.Fields) logger.Logger { 70 | return &logrusLogEntry{ 71 | entry: l.entry.WithFields(convertToLogrusFields(fields)), 72 | } 73 | } 74 | 75 | func (l *logrusLogEntry) WithError(err error) logger.Logger { 76 | return &logrusLogEntry{ 77 | entry: l.entry.WithError(err), 78 | } 79 | } 80 | 81 | func convertToLogrusFields(fields logger.Fields) logrus.Fields { 82 | logrusFields := logrus.Fields{} 83 | for index, field := range fields { 84 | logrusFields[index] = field 85 | } 86 | 87 | return logrusFields 88 | } 89 | -------------------------------------------------------------------------------- /infrastructure/log/zap.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | type zapLogger struct { 9 | logger *zap.SugaredLogger 10 | } 11 | 12 | func NewZapLogger() (logger.Logger, error) { 13 | log, err := zap.NewProduction() 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | sugar := log.Sugar() 19 | defer log.Sync() 20 | 21 | return &zapLogger{logger: sugar}, nil 22 | } 23 | 24 | func (l *zapLogger) Infof(format string, args ...interface{}) { 25 | l.logger.Infof(format, args...) 26 | } 27 | 28 | func (l *zapLogger) Warnf(format string, args ...interface{}) { 29 | l.logger.Warnf(format, args...) 30 | } 31 | 32 | func (l *zapLogger) Errorf(format string, args ...interface{}) { 33 | l.logger.Errorf(format, args...) 34 | } 35 | 36 | func (l *zapLogger) Fatalln(args ...interface{}) { 37 | l.logger.Fatal(args) 38 | } 39 | 40 | func (l *zapLogger) WithFields(fields logger.Fields) logger.Logger { 41 | var f = make([]interface{}, 0) 42 | for index, field := range fields { 43 | f = append(f, index) 44 | f = append(f, field) 45 | } 46 | 47 | log := l.logger.With(f...) 48 | return &zapLogger{logger: log} 49 | } 50 | 51 | func (l *zapLogger) WithError(err error) logger.Logger { 52 | var log = l.logger.With(err.Error()) 53 | return &zapLogger{logger: log} 54 | } 55 | -------------------------------------------------------------------------------- /infrastructure/router/factory.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "errors" 5 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 6 | "time" 7 | 8 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 9 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 10 | ) 11 | 12 | type Server interface { 13 | Listen() 14 | } 15 | 16 | type Port int64 17 | 18 | var ( 19 | errInvalidWebServerInstance = errors.New("invalid router server instance") 20 | ) 21 | 22 | const ( 23 | InstanceGorillaMux int = iota 24 | InstanceGin 25 | ) 26 | 27 | func NewWebServerFactory( 28 | instance int, 29 | log logger.Logger, 30 | dbSQL repository.SQL, 31 | dbNoSQL repository.NoSQL, 32 | validator validator.Validator, 33 | port Port, 34 | ctxTimeout time.Duration, 35 | ) (Server, error) { 36 | switch instance { 37 | case InstanceGorillaMux: 38 | return newGorillaMux(log, dbSQL, validator, port, ctxTimeout), nil 39 | case InstanceGin: 40 | return newGinServer(log, dbNoSQL, validator, port, ctxTimeout), nil 41 | default: 42 | return nil, errInvalidWebServerInstance 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /infrastructure/router/gin.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/gsabadini/go-clean-architecture/adapter/api/action" 14 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 15 | "github.com/gsabadini/go-clean-architecture/adapter/presenter" 16 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 17 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 18 | "github.com/gsabadini/go-clean-architecture/usecase" 19 | ) 20 | 21 | type ginEngine struct { 22 | router *gin.Engine 23 | log logger.Logger 24 | db repository.NoSQL 25 | validator validator.Validator 26 | port Port 27 | ctxTimeout time.Duration 28 | } 29 | 30 | func newGinServer( 31 | log logger.Logger, 32 | db repository.NoSQL, 33 | validator validator.Validator, 34 | port Port, 35 | t time.Duration, 36 | ) *ginEngine { 37 | return &ginEngine{ 38 | router: gin.New(), 39 | log: log, 40 | db: db, 41 | validator: validator, 42 | port: port, 43 | ctxTimeout: t, 44 | } 45 | } 46 | 47 | func (g ginEngine) Listen() { 48 | gin.SetMode(gin.ReleaseMode) 49 | gin.Recovery() 50 | 51 | g.setAppHandlers(g.router) 52 | 53 | server := &http.Server{ 54 | ReadTimeout: 5 * time.Second, 55 | WriteTimeout: 15 * time.Second, 56 | Addr: fmt.Sprintf(":%d", g.port), 57 | Handler: g.router, 58 | } 59 | 60 | stop := make(chan os.Signal, 1) 61 | signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 62 | 63 | go func() { 64 | g.log.WithFields(logger.Fields{"port": g.port}).Infof("Starting HTTP Server") 65 | if err := server.ListenAndServe(); err != nil { 66 | g.log.WithError(err).Fatalln("Error starting HTTP server") 67 | } 68 | }() 69 | 70 | <-stop 71 | 72 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 73 | defer func() { 74 | cancel() 75 | }() 76 | 77 | if err := server.Shutdown(ctx); err != nil { 78 | g.log.WithError(err).Fatalln("Server Shutdown Failed") 79 | } 80 | 81 | g.log.Infof("Service down") 82 | } 83 | 84 | /* TODO ADD MIDDLEWARE */ 85 | func (g ginEngine) setAppHandlers(router *gin.Engine) { 86 | router.POST("/v1/transfers", g.buildCreateTransferAction()) 87 | router.GET("/v1/transfers", g.buildFindAllTransferAction()) 88 | 89 | router.GET("/v1/accounts/:account_id/balance", g.buildFindBalanceAccountAction()) 90 | router.POST("/v1/accounts", g.buildCreateAccountAction()) 91 | router.GET("/v1/accounts", g.buildFindAllAccountAction()) 92 | 93 | router.GET("/v1/health", g.healthcheck()) 94 | } 95 | 96 | func (g ginEngine) buildCreateTransferAction() gin.HandlerFunc { 97 | return func(c *gin.Context) { 98 | var ( 99 | uc = usecase.NewCreateTransferInteractor( 100 | repository.NewTransferNoSQL(g.db), 101 | repository.NewAccountNoSQL(g.db), 102 | presenter.NewCreateTransferPresenter(), 103 | g.ctxTimeout, 104 | ) 105 | 106 | act = action.NewCreateTransferAction(uc, g.log, g.validator) 107 | ) 108 | 109 | act.Execute(c.Writer, c.Request) 110 | } 111 | } 112 | 113 | func (g ginEngine) buildFindAllTransferAction() gin.HandlerFunc { 114 | return func(c *gin.Context) { 115 | var ( 116 | uc = usecase.NewFindAllTransferInteractor( 117 | repository.NewTransferNoSQL(g.db), 118 | presenter.NewFindAllTransferPresenter(), 119 | g.ctxTimeout, 120 | ) 121 | act = action.NewFindAllTransferAction(uc, g.log) 122 | ) 123 | 124 | act.Execute(c.Writer, c.Request) 125 | } 126 | } 127 | 128 | func (g ginEngine) buildCreateAccountAction() gin.HandlerFunc { 129 | return func(c *gin.Context) { 130 | var ( 131 | uc = usecase.NewCreateAccountInteractor( 132 | repository.NewAccountNoSQL(g.db), 133 | presenter.NewCreateAccountPresenter(), 134 | g.ctxTimeout, 135 | ) 136 | act = action.NewCreateAccountAction(uc, g.log, g.validator) 137 | ) 138 | 139 | act.Execute(c.Writer, c.Request) 140 | } 141 | } 142 | 143 | func (g ginEngine) buildFindAllAccountAction() gin.HandlerFunc { 144 | return func(c *gin.Context) { 145 | var ( 146 | uc = usecase.NewFindAllAccountInteractor( 147 | repository.NewAccountNoSQL(g.db), 148 | presenter.NewFindAllAccountPresenter(), 149 | g.ctxTimeout, 150 | ) 151 | act = action.NewFindAllAccountAction(uc, g.log) 152 | ) 153 | 154 | act.Execute(c.Writer, c.Request) 155 | } 156 | } 157 | 158 | func (g ginEngine) buildFindBalanceAccountAction() gin.HandlerFunc { 159 | return func(c *gin.Context) { 160 | var ( 161 | uc = usecase.NewFindBalanceAccountInteractor( 162 | repository.NewAccountNoSQL(g.db), 163 | presenter.NewFindAccountBalancePresenter(), 164 | g.ctxTimeout, 165 | ) 166 | act = action.NewFindAccountBalanceAction(uc, g.log) 167 | ) 168 | 169 | q := c.Request.URL.Query() 170 | q.Add("account_id", c.Param("account_id")) 171 | c.Request.URL.RawQuery = q.Encode() 172 | 173 | act.Execute(c.Writer, c.Request) 174 | } 175 | } 176 | 177 | func (g ginEngine) healthcheck() gin.HandlerFunc { 178 | return func(c *gin.Context) { 179 | action.HealthCheck(c.Writer, c.Request) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /infrastructure/router/gorilla_mux.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/gsabadini/go-clean-architecture/adapter/api/action" 13 | "github.com/gsabadini/go-clean-architecture/adapter/api/middleware" 14 | "github.com/gsabadini/go-clean-architecture/adapter/logger" 15 | "github.com/gsabadini/go-clean-architecture/adapter/presenter" 16 | "github.com/gsabadini/go-clean-architecture/adapter/repository" 17 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 18 | "github.com/gsabadini/go-clean-architecture/usecase" 19 | 20 | "github.com/gorilla/mux" 21 | "github.com/urfave/negroni" 22 | ) 23 | 24 | type gorillaMux struct { 25 | router *mux.Router 26 | middleware *negroni.Negroni 27 | log logger.Logger 28 | db repository.SQL 29 | validator validator.Validator 30 | port Port 31 | ctxTimeout time.Duration 32 | } 33 | 34 | func newGorillaMux( 35 | log logger.Logger, 36 | db repository.SQL, 37 | validator validator.Validator, 38 | port Port, 39 | t time.Duration, 40 | ) *gorillaMux { 41 | return &gorillaMux{ 42 | router: mux.NewRouter(), 43 | middleware: negroni.New(), 44 | log: log, 45 | db: db, 46 | validator: validator, 47 | port: port, 48 | ctxTimeout: t, 49 | } 50 | } 51 | 52 | func (g gorillaMux) Listen() { 53 | g.setAppHandlers(g.router) 54 | g.middleware.UseHandler(g.router) 55 | 56 | server := &http.Server{ 57 | ReadTimeout: 5 * time.Second, 58 | WriteTimeout: 15 * time.Second, 59 | Addr: fmt.Sprintf(":%d", g.port), 60 | Handler: g.middleware, 61 | } 62 | 63 | stop := make(chan os.Signal, 1) 64 | signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 65 | 66 | go func() { 67 | g.log.WithFields(logger.Fields{"port": g.port}).Infof("Starting HTTP Server") 68 | if err := server.ListenAndServe(); err != nil { 69 | g.log.WithError(err).Fatalln("Error starting HTTP server") 70 | } 71 | }() 72 | 73 | <-stop 74 | 75 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 76 | defer func() { 77 | cancel() 78 | }() 79 | 80 | if err := server.Shutdown(ctx); err != nil { 81 | g.log.WithError(err).Fatalln("Server Shutdown Failed") 82 | } 83 | 84 | g.log.Infof("Service down") 85 | } 86 | 87 | func (g gorillaMux) setAppHandlers(router *mux.Router) { 88 | api := router.PathPrefix("/v1").Subrouter() 89 | 90 | api.Handle("/transfers", g.buildCreateTransferAction()).Methods(http.MethodPost) 91 | api.Handle("/transfers", g.buildFindAllTransferAction()).Methods(http.MethodGet) 92 | 93 | api.Handle("/accounts/{account_id}/balance", g.buildFindBalanceAccountAction()).Methods(http.MethodGet) 94 | api.Handle("/accounts", g.buildCreateAccountAction()).Methods(http.MethodPost) 95 | api.Handle("/accounts", g.buildFindAllAccountAction()).Methods(http.MethodGet) 96 | 97 | api.HandleFunc("/health", action.HealthCheck).Methods(http.MethodGet) 98 | } 99 | 100 | func (g gorillaMux) buildCreateTransferAction() *negroni.Negroni { 101 | var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) { 102 | var ( 103 | uc = usecase.NewCreateTransferInteractor( 104 | repository.NewTransferSQL(g.db), 105 | repository.NewAccountSQL(g.db), 106 | presenter.NewCreateTransferPresenter(), 107 | g.ctxTimeout, 108 | ) 109 | act = action.NewCreateTransferAction(uc, g.log, g.validator) 110 | ) 111 | 112 | act.Execute(res, req) 113 | } 114 | 115 | return negroni.New( 116 | negroni.HandlerFunc(middleware.NewLogger(g.log).Execute), 117 | negroni.NewRecovery(), 118 | negroni.Wrap(handler), 119 | ) 120 | } 121 | 122 | func (g gorillaMux) buildFindAllTransferAction() *negroni.Negroni { 123 | var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) { 124 | var ( 125 | uc = usecase.NewFindAllTransferInteractor( 126 | repository.NewTransferSQL(g.db), 127 | presenter.NewFindAllTransferPresenter(), 128 | g.ctxTimeout, 129 | ) 130 | act = action.NewFindAllTransferAction(uc, g.log) 131 | ) 132 | 133 | act.Execute(res, req) 134 | } 135 | 136 | return negroni.New( 137 | negroni.HandlerFunc(middleware.NewLogger(g.log).Execute), 138 | negroni.NewRecovery(), 139 | negroni.Wrap(handler), 140 | ) 141 | } 142 | 143 | func (g gorillaMux) buildCreateAccountAction() *negroni.Negroni { 144 | var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) { 145 | var ( 146 | uc = usecase.NewCreateAccountInteractor( 147 | repository.NewAccountSQL(g.db), 148 | presenter.NewCreateAccountPresenter(), 149 | g.ctxTimeout, 150 | ) 151 | act = action.NewCreateAccountAction(uc, g.log, g.validator) 152 | ) 153 | 154 | act.Execute(res, req) 155 | } 156 | 157 | return negroni.New( 158 | negroni.HandlerFunc(middleware.NewLogger(g.log).Execute), 159 | negroni.NewRecovery(), 160 | negroni.Wrap(handler), 161 | ) 162 | } 163 | 164 | func (g gorillaMux) buildFindAllAccountAction() *negroni.Negroni { 165 | var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) { 166 | var ( 167 | uc = usecase.NewFindAllAccountInteractor( 168 | repository.NewAccountSQL(g.db), 169 | presenter.NewFindAllAccountPresenter(), 170 | g.ctxTimeout, 171 | ) 172 | act = action.NewFindAllAccountAction(uc, g.log) 173 | ) 174 | 175 | act.Execute(res, req) 176 | } 177 | 178 | return negroni.New( 179 | negroni.HandlerFunc(middleware.NewLogger(g.log).Execute), 180 | negroni.NewRecovery(), 181 | negroni.Wrap(handler), 182 | ) 183 | } 184 | 185 | func (g gorillaMux) buildFindBalanceAccountAction() *negroni.Negroni { 186 | var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) { 187 | var ( 188 | uc = usecase.NewFindBalanceAccountInteractor( 189 | repository.NewAccountSQL(g.db), 190 | presenter.NewFindAccountBalancePresenter(), 191 | g.ctxTimeout, 192 | ) 193 | act = action.NewFindAccountBalanceAction(uc, g.log) 194 | ) 195 | 196 | var ( 197 | vars = mux.Vars(req) 198 | q = req.URL.Query() 199 | ) 200 | 201 | q.Add("account_id", vars["account_id"]) 202 | req.URL.RawQuery = q.Encode() 203 | 204 | act.Execute(res, req) 205 | } 206 | 207 | return negroni.New( 208 | negroni.HandlerFunc(middleware.NewLogger(g.log).Execute), 209 | negroni.NewRecovery(), 210 | negroni.Wrap(handler), 211 | ) 212 | } 213 | -------------------------------------------------------------------------------- /infrastructure/validation/factory.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 7 | ) 8 | 9 | var ( 10 | errInvalidValidatorInstance = errors.New("invalid validator instance") 11 | ) 12 | 13 | const ( 14 | InstanceGoPlayground int = iota 15 | ) 16 | 17 | func NewValidatorFactory(instance int) (validator.Validator, error) { 18 | switch instance { 19 | case InstanceGoPlayground: 20 | return NewGoPlayground() 21 | default: 22 | return nil, errInvalidValidatorInstance 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /infrastructure/validation/go_playground.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gsabadini/go-clean-architecture/adapter/validator" 7 | 8 | "github.com/go-playground/locales/en" 9 | ut "github.com/go-playground/universal-translator" 10 | go_playground "github.com/go-playground/validator/v10" 11 | en_translations "github.com/go-playground/validator/v10/translations/en" 12 | ) 13 | 14 | type goPlayground struct { 15 | validator *go_playground.Validate 16 | translate ut.Translator 17 | err error 18 | msg []string 19 | } 20 | 21 | func NewGoPlayground() (validator.Validator, error) { 22 | var ( 23 | language = en.New() 24 | uni = ut.New(language, language) 25 | translate, found = uni.GetTranslator("en") 26 | ) 27 | 28 | if !found { 29 | return nil, errors.New("translator not found") 30 | } 31 | 32 | v := go_playground.New() 33 | if err := en_translations.RegisterDefaultTranslations(v, translate); err != nil { 34 | return nil, errors.New("translator not found") 35 | } 36 | 37 | return &goPlayground{validator: v, translate: translate}, nil 38 | } 39 | 40 | func (g *goPlayground) Validate(i interface{}) error { 41 | if len(g.msg) > 0 { 42 | g.msg = nil 43 | } 44 | 45 | g.err = g.validator.Struct(i) 46 | if g.err != nil { 47 | return g.err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (g *goPlayground) Messages() []string { 54 | if g.err != nil { 55 | for _, err := range g.err.(go_playground.ValidationErrors) { 56 | g.msg = append(g.msg, err.Translate(g.translate)) 57 | } 58 | } 59 | 60 | return g.msg 61 | } 62 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/infrastructure" 8 | "github.com/gsabadini/go-clean-architecture/infrastructure/database" 9 | "github.com/gsabadini/go-clean-architecture/infrastructure/log" 10 | "github.com/gsabadini/go-clean-architecture/infrastructure/router" 11 | "github.com/gsabadini/go-clean-architecture/infrastructure/validation" 12 | ) 13 | 14 | func main() { 15 | var app = infrastructure.NewConfig(). 16 | Name(os.Getenv("APP_NAME")). 17 | ContextTimeout(10 * time.Second). 18 | Logger(log.InstanceLogrusLogger). 19 | Validator(validation.InstanceGoPlayground). 20 | DbSQL(database.InstancePostgres). 21 | DbNoSQL(database.InstanceMongoDB) 22 | 23 | app.WebServerPort(os.Getenv("APP_PORT")). 24 | WebServer(router.InstanceGorillaMux). 25 | Start() 26 | } 27 | -------------------------------------------------------------------------------- /usecase/create_account.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | ) 9 | 10 | type ( 11 | // CreateAccountUseCase input port 12 | CreateAccountUseCase interface { 13 | Execute(context.Context, CreateAccountInput) (CreateAccountOutput, error) 14 | } 15 | 16 | // CreateAccountInput input data 17 | CreateAccountInput struct { 18 | Name string `json:"name" validate:"required"` 19 | CPF string `json:"cpf" validate:"required"` 20 | Balance int64 `json:"balance" validate:"gt=0,required"` 21 | } 22 | 23 | // CreateAccountPresenter output port 24 | CreateAccountPresenter interface { 25 | Output(domain.Account) CreateAccountOutput 26 | } 27 | 28 | // CreateAccountOutput output data 29 | CreateAccountOutput struct { 30 | ID string `json:"id"` 31 | Name string `json:"name"` 32 | CPF string `json:"cpf"` 33 | Balance float64 `json:"balance"` 34 | CreatedAt string `json:"created_at"` 35 | } 36 | 37 | createAccountInteractor struct { 38 | repo domain.AccountRepository 39 | presenter CreateAccountPresenter 40 | ctxTimeout time.Duration 41 | } 42 | ) 43 | 44 | // NewCreateAccountInteractor creates new createAccountInteractor with its dependencies 45 | func NewCreateAccountInteractor( 46 | repo domain.AccountRepository, 47 | presenter CreateAccountPresenter, 48 | t time.Duration, 49 | ) CreateAccountUseCase { 50 | return createAccountInteractor{ 51 | repo: repo, 52 | presenter: presenter, 53 | ctxTimeout: t, 54 | } 55 | } 56 | 57 | // Execute orchestrates the use case 58 | func (a createAccountInteractor) Execute(ctx context.Context, input CreateAccountInput) (CreateAccountOutput, error) { 59 | ctx, cancel := context.WithTimeout(ctx, a.ctxTimeout) 60 | defer cancel() 61 | 62 | var account = domain.NewAccount( 63 | domain.AccountID(domain.NewUUID()), 64 | input.Name, 65 | input.CPF, 66 | domain.Money(input.Balance), 67 | time.Now(), 68 | ) 69 | 70 | account, err := a.repo.Create(ctx, account) 71 | if err != nil { 72 | return a.presenter.Output(domain.Account{}), err 73 | } 74 | 75 | return a.presenter.Output(account), nil 76 | } 77 | -------------------------------------------------------------------------------- /usecase/create_account_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gsabadini/go-clean-architecture/domain" 11 | ) 12 | 13 | type mockAccountRepoStore struct { 14 | domain.AccountRepository 15 | 16 | result domain.Account 17 | err error 18 | } 19 | 20 | func (m mockAccountRepoStore) Create(_ context.Context, _ domain.Account) (domain.Account, error) { 21 | return m.result, m.err 22 | } 23 | 24 | type mockCreateAccountPresenter struct { 25 | result CreateAccountOutput 26 | } 27 | 28 | func (m mockCreateAccountPresenter) Output(_ domain.Account) CreateAccountOutput { 29 | return m.result 30 | } 31 | 32 | func TestCreateAccountInteractor_Execute(t *testing.T) { 33 | t.Parallel() 34 | 35 | type args struct { 36 | input CreateAccountInput 37 | } 38 | 39 | tests := []struct { 40 | name string 41 | args args 42 | repository domain.AccountRepository 43 | presenter CreateAccountPresenter 44 | expected CreateAccountOutput 45 | expectedError interface{} 46 | }{ 47 | { 48 | name: "Create account successful", 49 | args: args{ 50 | input: CreateAccountInput{ 51 | Name: "Test", 52 | CPF: "02815517078", 53 | Balance: 19944, 54 | }, 55 | }, 56 | repository: mockAccountRepoStore{ 57 | result: domain.NewAccount( 58 | "3c096a40-ccba-4b58-93ed-57379ab04680", 59 | "Test", 60 | "02815517078", 61 | 19944, 62 | time.Time{}, 63 | ), 64 | err: nil, 65 | }, 66 | presenter: mockCreateAccountPresenter{ 67 | result: CreateAccountOutput{ 68 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 69 | Name: "Test", 70 | CPF: "02815517078", 71 | Balance: 199.44, 72 | CreatedAt: time.Time{}.String(), 73 | }, 74 | }, 75 | expected: CreateAccountOutput{ 76 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 77 | Name: "Test", 78 | CPF: "02815517078", 79 | Balance: 199.44, 80 | CreatedAt: time.Time{}.String(), 81 | }, 82 | }, 83 | { 84 | name: "Create account successful", 85 | args: args{ 86 | input: CreateAccountInput{ 87 | Name: "Test", 88 | CPF: "02815517078", 89 | Balance: 2350, 90 | }, 91 | }, 92 | repository: mockAccountRepoStore{ 93 | result: domain.NewAccount( 94 | "3c096a40-ccba-4b58-93ed-57379ab04680", 95 | "Test", 96 | "02815517078", 97 | 2350, 98 | time.Time{}, 99 | ), 100 | err: nil, 101 | }, 102 | presenter: mockCreateAccountPresenter{ 103 | result: CreateAccountOutput{ 104 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 105 | Name: "Test", 106 | CPF: "02815517078", 107 | Balance: 23.5, 108 | CreatedAt: time.Time{}.String(), 109 | }, 110 | }, 111 | expected: CreateAccountOutput{ 112 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 113 | Name: "Test", 114 | CPF: "02815517078", 115 | Balance: 23.5, 116 | CreatedAt: time.Time{}.String(), 117 | }, 118 | }, 119 | { 120 | name: "Create account generic error", 121 | args: args{ 122 | input: CreateAccountInput{ 123 | Name: "", 124 | CPF: "", 125 | Balance: 0, 126 | }, 127 | }, 128 | repository: mockAccountRepoStore{ 129 | result: domain.Account{}, 130 | err: errors.New("error"), 131 | }, 132 | presenter: mockCreateAccountPresenter{ 133 | result: CreateAccountOutput{}, 134 | }, 135 | expectedError: "error", 136 | expected: CreateAccountOutput{}, 137 | }, 138 | } 139 | 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | var uc = NewCreateAccountInteractor(tt.repository, tt.presenter, time.Second) 143 | 144 | result, err := uc.Execute(context.TODO(), tt.args.input) 145 | if (err != nil) && (err.Error() != tt.expectedError) { 146 | t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError) 147 | } 148 | 149 | if !reflect.DeepEqual(result, tt.expected) { 150 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected) 151 | } 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /usecase/create_transfer.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | ) 9 | 10 | type ( 11 | // CreateTransferUseCase input port 12 | CreateTransferUseCase interface { 13 | Execute(context.Context, CreateTransferInput) (CreateTransferOutput, error) 14 | } 15 | 16 | // CreateTransferInput input data 17 | CreateTransferInput struct { 18 | AccountOriginID string `json:"account_origin_id" validate:"required,uuid4"` 19 | AccountDestinationID string `json:"account_destination_id" validate:"required,uuid4"` 20 | Amount int64 `json:"amount" validate:"gt=0,required"` 21 | } 22 | 23 | // CreateTransferPresenter output port 24 | CreateTransferPresenter interface { 25 | Output(domain.Transfer) CreateTransferOutput 26 | } 27 | 28 | // CreateTransferOutput output data 29 | CreateTransferOutput struct { 30 | ID string `json:"id"` 31 | AccountOriginID string `json:"account_origin_id"` 32 | AccountDestinationID string `json:"account_destination_id"` 33 | Amount float64 `json:"amount"` 34 | CreatedAt string `json:"created_at"` 35 | } 36 | 37 | createTransferInteractor struct { 38 | transferRepo domain.TransferRepository 39 | accountRepo domain.AccountRepository 40 | presenter CreateTransferPresenter 41 | ctxTimeout time.Duration 42 | } 43 | ) 44 | 45 | // NewCreateTransferInteractor creates new createTransferInteractor with its dependencies 46 | func NewCreateTransferInteractor( 47 | transferRepo domain.TransferRepository, 48 | accountRepo domain.AccountRepository, 49 | presenter CreateTransferPresenter, 50 | t time.Duration, 51 | ) CreateTransferUseCase { 52 | return createTransferInteractor{ 53 | transferRepo: transferRepo, 54 | accountRepo: accountRepo, 55 | presenter: presenter, 56 | ctxTimeout: t, 57 | } 58 | } 59 | 60 | // Execute orchestrates the use case 61 | func (t createTransferInteractor) Execute(ctx context.Context, input CreateTransferInput) (CreateTransferOutput, error) { 62 | ctx, cancel := context.WithTimeout(ctx, t.ctxTimeout) 63 | defer cancel() 64 | 65 | var ( 66 | transfer domain.Transfer 67 | err error 68 | ) 69 | 70 | err = t.transferRepo.WithTransaction(ctx, func(ctxTx context.Context) error { 71 | if err = t.process(ctxTx, input); err != nil { 72 | return err 73 | } 74 | 75 | transfer = domain.NewTransfer( 76 | domain.TransferID(domain.NewUUID()), 77 | domain.AccountID(input.AccountOriginID), 78 | domain.AccountID(input.AccountDestinationID), 79 | domain.Money(input.Amount), 80 | time.Now(), 81 | ) 82 | 83 | transfer, err = t.transferRepo.Create(ctxTx, transfer) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | }) 90 | if err != nil { 91 | return t.presenter.Output(domain.Transfer{}), err 92 | } 93 | 94 | return t.presenter.Output(transfer), nil 95 | } 96 | 97 | func (t createTransferInteractor) process(ctx context.Context, input CreateTransferInput) error { 98 | origin, err := t.accountRepo.FindByID(ctx, domain.AccountID(input.AccountOriginID)) 99 | if err != nil { 100 | switch err { 101 | case domain.ErrAccountNotFound: 102 | return domain.ErrAccountOriginNotFound 103 | default: 104 | return err 105 | } 106 | } 107 | 108 | if err := origin.Withdraw(domain.Money(input.Amount)); err != nil { 109 | return err 110 | } 111 | 112 | destination, err := t.accountRepo.FindByID(ctx, domain.AccountID(input.AccountDestinationID)) 113 | if err != nil { 114 | switch err { 115 | case domain.ErrAccountNotFound: 116 | return domain.ErrAccountDestinationNotFound 117 | default: 118 | return err 119 | } 120 | } 121 | 122 | destination.Deposit(domain.Money(input.Amount)) 123 | 124 | if err = t.accountRepo.UpdateBalance(ctx, origin.ID(), origin.Balance()); err != nil { 125 | return err 126 | } 127 | 128 | if err = t.accountRepo.UpdateBalance(ctx, destination.ID(), destination.Balance()); err != nil { 129 | return err 130 | } 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /usecase/create_transfer_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gsabadini/go-clean-architecture/domain" 11 | ) 12 | 13 | type mockTransferRepoStore struct { 14 | domain.TransferRepository 15 | 16 | result domain.Transfer 17 | err error 18 | } 19 | 20 | func (m mockTransferRepoStore) Create(_ context.Context, _ domain.Transfer) (domain.Transfer, error) { 21 | return m.result, m.err 22 | } 23 | 24 | func (m mockTransferRepoStore) WithTransaction(_ context.Context, fn func(context.Context) error) error { 25 | if err := fn(context.Background()); err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | type invoked struct { 33 | call bool 34 | } 35 | 36 | type mockAccountRepo struct { 37 | domain.AccountRepository 38 | 39 | updateBalanceOriginFake func() error 40 | updateBalanceDestinationFake func() error 41 | invokedUpdate *invoked 42 | 43 | findByIDOriginFake func() (domain.Account, error) 44 | findByIDDestinationFake func() (domain.Account, error) 45 | invokedFind *invoked 46 | } 47 | 48 | func (m mockAccountRepo) UpdateBalance(_ context.Context, _ domain.AccountID, _ domain.Money) error { 49 | if m.invokedUpdate != nil && m.invokedUpdate.call { 50 | return m.updateBalanceDestinationFake() 51 | } 52 | 53 | if m.invokedUpdate != nil { 54 | m.invokedUpdate.call = true 55 | } 56 | return m.updateBalanceOriginFake() 57 | } 58 | 59 | func (m mockAccountRepo) FindByID(_ context.Context, _ domain.AccountID) (domain.Account, error) { 60 | if m.invokedFind != nil && m.invokedFind.call { 61 | return m.findByIDDestinationFake() 62 | } 63 | 64 | if m.invokedFind != nil { 65 | m.invokedFind.call = true 66 | } 67 | return m.findByIDOriginFake() 68 | } 69 | 70 | type mockCreateTransferPresenter struct { 71 | result CreateTransferOutput 72 | } 73 | 74 | func (m mockCreateTransferPresenter) Output(_ domain.Transfer) CreateTransferOutput { 75 | return m.result 76 | } 77 | 78 | func TestTransferCreateInteractor_Execute(t *testing.T) { 79 | t.Parallel() 80 | 81 | type args struct { 82 | input CreateTransferInput 83 | } 84 | 85 | tests := []struct { 86 | name string 87 | args args 88 | transferRepo domain.TransferRepository 89 | accountRepo domain.AccountRepository 90 | presenter CreateTransferPresenter 91 | expected CreateTransferOutput 92 | expectedError string 93 | }{ 94 | { 95 | name: "Create transfer successful", 96 | args: args{input: CreateTransferInput{ 97 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 98 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 99 | Amount: 2999, 100 | }}, 101 | transferRepo: mockTransferRepoStore{ 102 | result: domain.NewTransfer( 103 | "3c096a40-ccba-4b58-93ed-57379ab04680", 104 | "3c096a40-ccba-4b58-93ed-57379ab04681", 105 | "3c096a40-ccba-4b58-93ed-57379ab04682", 106 | 2999, 107 | time.Time{}, 108 | ), 109 | err: nil, 110 | }, 111 | accountRepo: mockAccountRepo{ 112 | updateBalanceOriginFake: func() error { 113 | return nil 114 | }, 115 | updateBalanceDestinationFake: func() error { 116 | return nil 117 | }, 118 | findByIDOriginFake: func() (domain.Account, error) { 119 | return domain.NewAccount( 120 | "3c096a40-ccba-4b58-93ed-57379ab04681", 121 | "Test", 122 | "08098565895", 123 | 5000, 124 | time.Time{}, 125 | ), nil 126 | }, 127 | findByIDDestinationFake: func() (domain.Account, error) { 128 | return domain.NewAccount( 129 | "3c096a40-ccba-4b58-93ed-57379ab04682", 130 | "Test2", 131 | "13098565491", 132 | 3000, 133 | time.Time{}, 134 | ), nil 135 | }, 136 | }, 137 | presenter: mockCreateTransferPresenter{ 138 | result: CreateTransferOutput{ 139 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 140 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 141 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 142 | Amount: 29.99, 143 | CreatedAt: time.Time{}.String(), 144 | }, 145 | }, 146 | expected: CreateTransferOutput{ 147 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 148 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 149 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 150 | Amount: 29.99, 151 | CreatedAt: time.Time{}.String(), 152 | }, 153 | }, 154 | { 155 | name: "Create transfer generic error transfer gateway", 156 | args: args{input: CreateTransferInput{ 157 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 158 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 159 | Amount: 200, 160 | }}, 161 | transferRepo: mockTransferRepoStore{ 162 | result: domain.Transfer{}, 163 | err: errors.New("error"), 164 | }, 165 | accountRepo: mockAccountRepo{ 166 | AccountRepository: nil, 167 | updateBalanceOriginFake: func() error { 168 | return nil 169 | }, 170 | updateBalanceDestinationFake: func() error { 171 | return nil 172 | }, 173 | findByIDOriginFake: func() (domain.Account, error) { 174 | return domain.NewAccount( 175 | "3c096a40-ccba-4b58-93ed-57379ab04681", 176 | "Test", 177 | "08098565895", 178 | 1000, 179 | time.Time{}, 180 | ), nil 181 | }, 182 | findByIDDestinationFake: func() (domain.Account, error) { 183 | return domain.NewAccount( 184 | "3c096a40-ccba-4b58-93ed-57379ab04682", 185 | "Test2", 186 | "13098565491", 187 | 3000, 188 | time.Time{}, 189 | ), nil 190 | }, 191 | }, 192 | presenter: mockCreateTransferPresenter{ 193 | result: CreateTransferOutput{}, 194 | }, 195 | expectedError: "error", 196 | expected: CreateTransferOutput{}, 197 | }, 198 | { 199 | name: "Create transfer error find origin account", 200 | args: args{input: CreateTransferInput{ 201 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 202 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 203 | Amount: 1999, 204 | }}, 205 | transferRepo: mockTransferRepoStore{ 206 | result: domain.Transfer{}, 207 | err: nil, 208 | }, 209 | accountRepo: mockAccountRepo{ 210 | updateBalanceOriginFake: func() error { 211 | return nil 212 | }, 213 | updateBalanceDestinationFake: func() error { 214 | return nil 215 | }, 216 | findByIDOriginFake: func() (domain.Account, error) { 217 | return domain.Account{}, errors.New("error") 218 | }, 219 | }, 220 | presenter: mockCreateTransferPresenter{ 221 | result: CreateTransferOutput{}, 222 | }, 223 | expectedError: "error", 224 | expected: CreateTransferOutput{}, 225 | }, 226 | { 227 | name: "Create transfer error not found find origin account", 228 | args: args{input: CreateTransferInput{ 229 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 230 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 231 | Amount: 1999, 232 | }}, 233 | transferRepo: mockTransferRepoStore{ 234 | result: domain.Transfer{}, 235 | err: nil, 236 | }, 237 | accountRepo: mockAccountRepo{ 238 | updateBalanceOriginFake: func() error { 239 | return nil 240 | }, 241 | updateBalanceDestinationFake: func() error { 242 | return nil 243 | }, 244 | findByIDOriginFake: func() (domain.Account, error) { 245 | return domain.Account{}, domain.ErrAccountOriginNotFound 246 | }, 247 | }, 248 | presenter: mockCreateTransferPresenter{ 249 | result: CreateTransferOutput{}, 250 | }, 251 | expectedError: "account origin not found", 252 | expected: CreateTransferOutput{}, 253 | }, 254 | { 255 | name: "Create transfer error find destination account", 256 | args: args{input: CreateTransferInput{ 257 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 258 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 259 | Amount: 100, 260 | }}, 261 | transferRepo: mockTransferRepoStore{ 262 | result: domain.Transfer{}, 263 | err: nil, 264 | }, 265 | accountRepo: &mockAccountRepo{ 266 | updateBalanceOriginFake: func() error { 267 | return nil 268 | }, 269 | updateBalanceDestinationFake: func() error { 270 | return nil 271 | }, 272 | findByIDOriginFake: func() (domain.Account, error) { 273 | return domain.NewAccount( 274 | "3c096a40-ccba-4b58-93ed-57379ab04681", 275 | "Test", 276 | "08098565895", 277 | 5000, 278 | time.Time{}, 279 | ), nil 280 | }, 281 | findByIDDestinationFake: func() (domain.Account, error) { 282 | return domain.Account{}, errors.New("error") 283 | }, 284 | invokedFind: &invoked{call: false}, 285 | }, 286 | presenter: mockCreateTransferPresenter{ 287 | result: CreateTransferOutput{}, 288 | }, 289 | expectedError: "error", 290 | expected: CreateTransferOutput{}, 291 | }, 292 | { 293 | name: "Create transfer error not found find destination account", 294 | args: args{input: CreateTransferInput{ 295 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 296 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 297 | Amount: 100, 298 | }}, 299 | transferRepo: mockTransferRepoStore{ 300 | result: domain.Transfer{}, 301 | err: nil, 302 | }, 303 | accountRepo: &mockAccountRepo{ 304 | updateBalanceOriginFake: func() error { 305 | return nil 306 | }, 307 | updateBalanceDestinationFake: func() error { 308 | return nil 309 | }, 310 | findByIDOriginFake: func() (domain.Account, error) { 311 | return domain.NewAccount( 312 | "3c096a40-ccba-4b58-93ed-57379ab04681", 313 | "Test", 314 | "08098565895", 315 | 5000, 316 | time.Time{}, 317 | ), nil 318 | }, 319 | findByIDDestinationFake: func() (domain.Account, error) { 320 | return domain.Account{}, domain.ErrAccountDestinationNotFound 321 | }, 322 | invokedFind: &invoked{call: false}, 323 | }, 324 | presenter: mockCreateTransferPresenter{ 325 | result: CreateTransferOutput{}, 326 | }, 327 | expectedError: "account destination not found", 328 | expected: CreateTransferOutput{}, 329 | }, 330 | { 331 | name: "Create transfer error update origin account", 332 | args: args{input: CreateTransferInput{ 333 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 334 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 335 | Amount: 250, 336 | }}, 337 | transferRepo: mockTransferRepoStore{ 338 | result: domain.Transfer{}, 339 | err: nil, 340 | }, 341 | accountRepo: mockAccountRepo{ 342 | updateBalanceOriginFake: func() error { 343 | return errors.New("error") 344 | }, 345 | updateBalanceDestinationFake: func() error { 346 | return nil 347 | }, 348 | findByIDOriginFake: func() (domain.Account, error) { 349 | return domain.NewAccount( 350 | "3c096a40-ccba-4b58-93ed-57379ab04681", 351 | "Test", 352 | "08098565895", 353 | 5999, 354 | time.Time{}, 355 | ), nil 356 | }, 357 | findByIDDestinationFake: func() (domain.Account, error) { 358 | return domain.NewAccount( 359 | "3c096a40-ccba-4b58-93ed-57379ab04682", 360 | "Test2", 361 | "13098565491", 362 | 2999, 363 | time.Time{}, 364 | ), nil 365 | }, 366 | }, 367 | presenter: mockCreateTransferPresenter{ 368 | result: CreateTransferOutput{}, 369 | }, 370 | expectedError: "error", 371 | expected: CreateTransferOutput{}, 372 | }, 373 | { 374 | name: "Create transfer error update destination account", 375 | args: args{input: CreateTransferInput{ 376 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 377 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 378 | Amount: 100, 379 | }}, 380 | transferRepo: mockTransferRepoStore{ 381 | result: domain.Transfer{}, 382 | err: nil, 383 | }, 384 | accountRepo: mockAccountRepo{ 385 | updateBalanceOriginFake: func() error { 386 | return nil 387 | }, 388 | updateBalanceDestinationFake: func() error { 389 | return errors.New("error") 390 | }, 391 | invokedUpdate: &invoked{call: false}, 392 | findByIDOriginFake: func() (domain.Account, error) { 393 | return domain.NewAccount( 394 | "3c096a40-ccba-4b58-93ed-57379ab04681", 395 | "Test", 396 | "08098565895", 397 | 200, 398 | time.Time{}, 399 | ), nil 400 | }, 401 | findByIDDestinationFake: func() (domain.Account, error) { 402 | return domain.NewAccount( 403 | "3c096a40-ccba-4b58-93ed-57379ab04682", 404 | "Test2", 405 | "13098565491", 406 | 100, 407 | time.Time{}, 408 | ), nil 409 | }, 410 | }, 411 | presenter: mockCreateTransferPresenter{ 412 | result: CreateTransferOutput{}, 413 | }, 414 | expectedError: "error", 415 | expected: CreateTransferOutput{}, 416 | }, 417 | { 418 | name: "Create transfer amount not have sufficient", 419 | args: args{input: CreateTransferInput{ 420 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04680", 421 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04681", 422 | Amount: 200, 423 | }}, 424 | transferRepo: mockTransferRepoStore{ 425 | result: domain.Transfer{}, 426 | err: nil, 427 | }, 428 | accountRepo: mockAccountRepo{ 429 | AccountRepository: nil, 430 | updateBalanceOriginFake: func() error { 431 | return nil 432 | }, 433 | updateBalanceDestinationFake: func() error { 434 | return nil 435 | }, 436 | findByIDOriginFake: func() (domain.Account, error) { 437 | return domain.NewAccount( 438 | "3c096a40-ccba-4b58-93ed-57379ab04681", 439 | "Test", 440 | "08098565895", 441 | 0, 442 | time.Time{}, 443 | ), nil 444 | }, 445 | findByIDDestinationFake: func() (domain.Account, error) { 446 | return domain.NewAccount( 447 | "3c096a40-ccba-4b58-93ed-57379ab04682", 448 | "Test2", 449 | "13098565491", 450 | 0, 451 | time.Time{}, 452 | ), nil 453 | }, 454 | }, 455 | presenter: mockCreateTransferPresenter{ 456 | result: CreateTransferOutput{}, 457 | }, 458 | expectedError: "origin account does not have sufficient balance", 459 | expected: CreateTransferOutput{}, 460 | }, 461 | } 462 | 463 | for _, tt := range tests { 464 | t.Run(tt.name, func(t *testing.T) { 465 | var uc = NewCreateTransferInteractor(tt.transferRepo, tt.accountRepo, tt.presenter, time.Second) 466 | 467 | got, err := uc.Execute(context.Background(), tt.args.input) 468 | if (err != nil) && (err.Error() != tt.expectedError) { 469 | t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError) 470 | return 471 | } 472 | 473 | if !reflect.DeepEqual(got, tt.expected) { 474 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, got, tt.expected) 475 | } 476 | }) 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /usecase/find_account_balance.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | ) 9 | 10 | type ( 11 | // FindAccountBalanceUseCase input port 12 | FindAccountBalanceUseCase interface { 13 | Execute(context.Context, domain.AccountID) (FindAccountBalanceOutput, error) 14 | } 15 | 16 | // FindAccountBalanceInput input data 17 | FindAccountBalanceInput struct { 18 | ID int64 `json:"balance" validate:"gt=0,required"` 19 | } 20 | 21 | // FindAccountBalancePresenter output port 22 | FindAccountBalancePresenter interface { 23 | Output(domain.Money) FindAccountBalanceOutput 24 | } 25 | 26 | // FindAccountBalanceOutput output data 27 | FindAccountBalanceOutput struct { 28 | Balance float64 `json:"balance"` 29 | } 30 | 31 | findBalanceAccountInteractor struct { 32 | repo domain.AccountRepository 33 | presenter FindAccountBalancePresenter 34 | ctxTimeout time.Duration 35 | } 36 | ) 37 | 38 | // NewFindBalanceAccountInteractor creates new findBalanceAccountInteractor with its dependencies 39 | func NewFindBalanceAccountInteractor( 40 | repo domain.AccountRepository, 41 | presenter FindAccountBalancePresenter, 42 | t time.Duration, 43 | ) FindAccountBalanceUseCase { 44 | return findBalanceAccountInteractor{ 45 | repo: repo, 46 | presenter: presenter, 47 | ctxTimeout: t, 48 | } 49 | } 50 | 51 | // Execute orchestrates the use case 52 | func (a findBalanceAccountInteractor) Execute(ctx context.Context, ID domain.AccountID) (FindAccountBalanceOutput, error) { 53 | ctx, cancel := context.WithTimeout(ctx, a.ctxTimeout) 54 | defer cancel() 55 | 56 | account, err := a.repo.FindBalance(ctx, ID) 57 | if err != nil { 58 | return a.presenter.Output(domain.Money(0)), err 59 | } 60 | 61 | return a.presenter.Output(account.Balance()), nil 62 | } 63 | -------------------------------------------------------------------------------- /usecase/find_account_balance_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gsabadini/go-clean-architecture/domain" 11 | ) 12 | 13 | type mockAccountRepoFindBalance struct { 14 | domain.AccountRepository 15 | 16 | result domain.Account 17 | err error 18 | } 19 | 20 | func (m mockAccountRepoFindBalance) FindBalance(_ context.Context, _ domain.AccountID) (domain.Account, error) { 21 | return m.result, m.err 22 | } 23 | 24 | type mockFindAccountBalancePresenter struct { 25 | result FindAccountBalanceOutput 26 | } 27 | 28 | func (m mockFindAccountBalancePresenter) Output(_ domain.Money) FindAccountBalanceOutput { 29 | return m.result 30 | } 31 | 32 | func TestFindBalanceAccountInteractor_Execute(t *testing.T) { 33 | t.Parallel() 34 | 35 | type args struct { 36 | ID domain.AccountID 37 | } 38 | 39 | tests := []struct { 40 | name string 41 | args args 42 | repository domain.AccountRepository 43 | presenter FindAccountBalancePresenter 44 | expected FindAccountBalanceOutput 45 | expectedError interface{} 46 | }{ 47 | { 48 | name: "Success when returning the account balance", 49 | args: args{ 50 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 51 | }, 52 | repository: mockAccountRepoFindBalance{ 53 | result: domain.NewAccountBalance(100), 54 | err: nil, 55 | }, 56 | presenter: mockFindAccountBalancePresenter{ 57 | result: FindAccountBalanceOutput{Balance: 1}, 58 | }, 59 | expected: FindAccountBalanceOutput{Balance: 1}, 60 | }, 61 | { 62 | name: "Success when returning the account balance", 63 | args: args{ 64 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 65 | }, 66 | repository: mockAccountRepoFindBalance{ 67 | result: domain.NewAccountBalance(20050), 68 | err: nil, 69 | }, 70 | presenter: mockFindAccountBalancePresenter{ 71 | result: FindAccountBalanceOutput{Balance: 200.5}, 72 | }, 73 | expected: FindAccountBalanceOutput{Balance: 200.5}, 74 | }, 75 | { 76 | name: "Error returning account balance", 77 | args: args{ 78 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 79 | }, 80 | repository: mockAccountRepoFindBalance{ 81 | result: domain.Account{}, 82 | err: errors.New("error"), 83 | }, 84 | presenter: mockFindAccountBalancePresenter{ 85 | result: FindAccountBalanceOutput{}, 86 | }, 87 | expectedError: "error", 88 | expected: FindAccountBalanceOutput{}, 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | var uc = NewFindBalanceAccountInteractor(tt.repository, tt.presenter, time.Second) 94 | 95 | result, err := uc.Execute(context.Background(), tt.args.ID) 96 | if (err != nil) && (err.Error() != tt.expectedError) { 97 | t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError) 98 | return 99 | } 100 | 101 | if !reflect.DeepEqual(result, tt.expected) { 102 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /usecase/find_all_account.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | ) 9 | 10 | type ( 11 | // FindAllAccountUseCase input port 12 | FindAllAccountUseCase interface { 13 | Execute(context.Context) ([]FindAllAccountOutput, error) 14 | } 15 | 16 | // FindAllAccountPresenter output port 17 | FindAllAccountPresenter interface { 18 | Output([]domain.Account) []FindAllAccountOutput 19 | } 20 | 21 | // FindAllAccountOutput outputData 22 | FindAllAccountOutput struct { 23 | ID string `json:"id"` 24 | Name string `json:"name"` 25 | CPF string `json:"cpf"` 26 | Balance float64 `json:"balance"` 27 | CreatedAt string `json:"created_at"` 28 | } 29 | 30 | findAllAccountInteractor struct { 31 | repo domain.AccountRepository 32 | presenter FindAllAccountPresenter 33 | ctxTimeout time.Duration 34 | } 35 | ) 36 | 37 | // NewFindAllAccountInteractor creates new findAllAccountInteractor with its dependencies 38 | func NewFindAllAccountInteractor( 39 | repo domain.AccountRepository, 40 | presenter FindAllAccountPresenter, 41 | t time.Duration, 42 | ) FindAllAccountUseCase { 43 | return findAllAccountInteractor{ 44 | repo: repo, 45 | presenter: presenter, 46 | ctxTimeout: t, 47 | } 48 | } 49 | 50 | // Execute orchestrates the use case 51 | func (a findAllAccountInteractor) Execute(ctx context.Context) ([]FindAllAccountOutput, error) { 52 | ctx, cancel := context.WithTimeout(ctx, a.ctxTimeout) 53 | defer cancel() 54 | 55 | accounts, err := a.repo.FindAll(ctx) 56 | if err != nil { 57 | return a.presenter.Output([]domain.Account{}), err 58 | } 59 | 60 | return a.presenter.Output(accounts), nil 61 | } 62 | -------------------------------------------------------------------------------- /usecase/find_all_account_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gsabadini/go-clean-architecture/domain" 11 | ) 12 | 13 | type mockAccountRepoFindAll struct { 14 | domain.AccountRepository 15 | 16 | result []domain.Account 17 | err error 18 | } 19 | 20 | func (m mockAccountRepoFindAll) FindAll(_ context.Context) ([]domain.Account, error) { 21 | return m.result, m.err 22 | } 23 | 24 | type mockFindAllAccountPresenter struct { 25 | result []FindAllAccountOutput 26 | } 27 | 28 | func (m mockFindAllAccountPresenter) Output(_ []domain.Account) []FindAllAccountOutput { 29 | return m.result 30 | } 31 | 32 | func TestFindAllAccountInteractor_Execute(t *testing.T) { 33 | t.Parallel() 34 | 35 | tests := []struct { 36 | name string 37 | repository domain.AccountRepository 38 | presenter FindAllAccountPresenter 39 | expected []FindAllAccountOutput 40 | expectedError interface{} 41 | }{ 42 | { 43 | name: "Success when returning the account list", 44 | repository: mockAccountRepoFindAll{ 45 | result: []domain.Account{ 46 | domain.NewAccount( 47 | "3c096a40-ccba-4b58-93ed-57379ab04680", 48 | "Test", 49 | "02815517078", 50 | 125, 51 | time.Time{}, 52 | ), 53 | domain.NewAccount( 54 | "3c096a40-ccba-4b58-93ed-57379ab04681", 55 | "Test", 56 | "02815517071", 57 | 99999, 58 | time.Time{}, 59 | ), 60 | }, 61 | err: nil, 62 | }, 63 | presenter: mockFindAllAccountPresenter{ 64 | result: []FindAllAccountOutput{ 65 | { 66 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 67 | Name: "Test", 68 | CPF: "02815517078", 69 | Balance: 1.25, 70 | CreatedAt: time.Time{}.String(), 71 | }, 72 | { 73 | ID: "3c096a40-ccba-4b58-93ed-57379ab04681", 74 | Name: "Test", 75 | CPF: "02815517071", 76 | Balance: 999.99, 77 | CreatedAt: time.Time{}.String(), 78 | }, 79 | }, 80 | }, 81 | expected: []FindAllAccountOutput{ 82 | { 83 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 84 | Name: "Test", 85 | CPF: "02815517078", 86 | Balance: 1.25, 87 | CreatedAt: time.Time{}.String(), 88 | }, 89 | { 90 | ID: "3c096a40-ccba-4b58-93ed-57379ab04681", 91 | Name: "Test", 92 | CPF: "02815517071", 93 | Balance: 999.99, 94 | CreatedAt: time.Time{}.String(), 95 | }, 96 | }, 97 | }, 98 | { 99 | name: "Success when returning the empty account list", 100 | repository: mockAccountRepoFindAll{ 101 | result: []domain.Account{}, 102 | err: nil, 103 | }, 104 | presenter: mockFindAllAccountPresenter{ 105 | result: []FindAllAccountOutput{}, 106 | }, 107 | expected: []FindAllAccountOutput{}, 108 | }, 109 | { 110 | name: "Error when returning the list of accounts", 111 | repository: mockAccountRepoFindAll{ 112 | result: []domain.Account{}, 113 | err: errors.New("error"), 114 | }, 115 | presenter: mockFindAllAccountPresenter{ 116 | result: []FindAllAccountOutput{}, 117 | }, 118 | expectedError: "error", 119 | expected: []FindAllAccountOutput{}, 120 | }, 121 | } 122 | 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | var uc = NewFindAllAccountInteractor(tt.repository, tt.presenter, time.Second) 126 | 127 | result, err := uc.Execute(context.Background()) 128 | if (err != nil) && (err.Error() != tt.expectedError) { 129 | t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError) 130 | } 131 | 132 | if !reflect.DeepEqual(result, tt.expected) { 133 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected) 134 | } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /usecase/find_all_transfer.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gsabadini/go-clean-architecture/domain" 8 | ) 9 | 10 | type ( 11 | // FindAllTransferUseCase input port 12 | FindAllTransferUseCase interface { 13 | Execute(context.Context) ([]FindAllTransferOutput, error) 14 | } 15 | 16 | // FindAllTransferPresenter output port 17 | FindAllTransferPresenter interface { 18 | Output([]domain.Transfer) []FindAllTransferOutput 19 | } 20 | 21 | // FindAllTransferOutput output data 22 | FindAllTransferOutput struct { 23 | ID string `json:"id"` 24 | AccountOriginID string `json:"account_origin_id"` 25 | AccountDestinationID string `json:"account_destination_id"` 26 | Amount float64 `json:"amount"` 27 | CreatedAt string `json:"created_at"` 28 | } 29 | 30 | findAllTransferInteractor struct { 31 | repo domain.TransferRepository 32 | presenter FindAllTransferPresenter 33 | ctxTimeout time.Duration 34 | } 35 | ) 36 | 37 | // NewFindAllTransferInteractor creates new findAllTransferInteractor with its dependencies 38 | func NewFindAllTransferInteractor( 39 | repo domain.TransferRepository, 40 | presenter FindAllTransferPresenter, 41 | t time.Duration, 42 | ) FindAllTransferUseCase { 43 | return findAllTransferInteractor{ 44 | repo: repo, 45 | presenter: presenter, 46 | ctxTimeout: t, 47 | } 48 | } 49 | 50 | // Execute orchestrates the use case 51 | func (t findAllTransferInteractor) Execute(ctx context.Context) ([]FindAllTransferOutput, error) { 52 | ctx, cancel := context.WithTimeout(ctx, t.ctxTimeout) 53 | defer cancel() 54 | 55 | transfers, err := t.repo.FindAll(ctx) 56 | if err != nil { 57 | return t.presenter.Output([]domain.Transfer{}), err 58 | } 59 | 60 | return t.presenter.Output(transfers), nil 61 | } 62 | -------------------------------------------------------------------------------- /usecase/find_all_transfer_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gsabadini/go-clean-architecture/domain" 11 | ) 12 | 13 | type mockTransferRepoFindAll struct { 14 | domain.TransferRepository 15 | 16 | result []domain.Transfer 17 | err error 18 | } 19 | 20 | func (m mockTransferRepoFindAll) FindAll(_ context.Context) ([]domain.Transfer, error) { 21 | return m.result, m.err 22 | } 23 | 24 | type mockFindAllTransferPresenter struct { 25 | result []FindAllTransferOutput 26 | } 27 | 28 | func (m mockFindAllTransferPresenter) Output(_ []domain.Transfer) []FindAllTransferOutput { 29 | return m.result 30 | } 31 | 32 | func TestTransferFindAllInteractor_Execute(t *testing.T) { 33 | t.Parallel() 34 | 35 | tests := []struct { 36 | name string 37 | expected []FindAllTransferOutput 38 | transferRepo domain.TransferRepository 39 | presenter FindAllTransferPresenter 40 | expectedError string 41 | }{ 42 | { 43 | name: "Success when returning the transfer list", 44 | transferRepo: mockTransferRepoFindAll{ 45 | result: []domain.Transfer{ 46 | domain.NewTransfer( 47 | "3c096a40-ccba-4b58-93ed-57379ab04680", 48 | "3c096a40-ccba-4b58-93ed-57379ab04681", 49 | "3c096a40-ccba-4b58-93ed-57379ab04682", 50 | 100, 51 | time.Time{}, 52 | ), 53 | domain.NewTransfer( 54 | "3c096a40-ccba-4b58-93ed-57379ab04680", 55 | "3c096a40-ccba-4b58-93ed-57379ab04681", 56 | "3c096a40-ccba-4b58-93ed-57379ab04682", 57 | 500, 58 | time.Time{}, 59 | ), 60 | }, 61 | err: nil, 62 | }, 63 | presenter: mockFindAllTransferPresenter{ 64 | result: []FindAllTransferOutput{ 65 | { 66 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 67 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 68 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 69 | Amount: 1, 70 | CreatedAt: time.Time{}.String(), 71 | }, 72 | { 73 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 74 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 75 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 76 | Amount: 5, 77 | CreatedAt: time.Time{}.String(), 78 | }, 79 | }, 80 | }, 81 | expected: []FindAllTransferOutput{ 82 | { 83 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 84 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 85 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 86 | Amount: 1, 87 | CreatedAt: time.Time{}.String(), 88 | }, 89 | { 90 | ID: "3c096a40-ccba-4b58-93ed-57379ab04680", 91 | AccountOriginID: "3c096a40-ccba-4b58-93ed-57379ab04681", 92 | AccountDestinationID: "3c096a40-ccba-4b58-93ed-57379ab04682", 93 | Amount: 5, 94 | CreatedAt: time.Time{}.String(), 95 | }, 96 | }, 97 | }, 98 | { 99 | name: "Success when returning the empty transfer list", 100 | transferRepo: mockTransferRepoFindAll{ 101 | result: []domain.Transfer{}, 102 | err: nil, 103 | }, 104 | presenter: mockFindAllTransferPresenter{ 105 | result: []FindAllTransferOutput{}, 106 | }, 107 | expected: []FindAllTransferOutput{}, 108 | }, 109 | { 110 | name: "Error when returning the transfer list", 111 | transferRepo: mockTransferRepoFindAll{ 112 | result: []domain.Transfer{}, 113 | err: errors.New("error"), 114 | }, 115 | presenter: mockFindAllTransferPresenter{ 116 | result: []FindAllTransferOutput{}, 117 | }, 118 | expected: []FindAllTransferOutput{}, 119 | expectedError: "error", 120 | }, 121 | } 122 | 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | var uc = NewFindAllTransferInteractor(tt.transferRepo, tt.presenter, time.Second) 126 | 127 | result, err := uc.Execute(context.Background()) 128 | if (err != nil) && (err.Error() != tt.expectedError) { 129 | t.Errorf("[TestCase '%s'] Result: '%v' | ExpectedError: '%v'", tt.name, err, tt.expectedError) 130 | return 131 | } 132 | 133 | if !reflect.DeepEqual(result, tt.expected) { 134 | t.Errorf("[TestCase '%s'] Result: '%v' | Expected: '%v'", tt.name, result, tt.expected) 135 | } 136 | }) 137 | } 138 | } 139 | --------------------------------------------------------------------------------