├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 | 
27 |
28 | ## Example create account use case
29 |
30 | 
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 |
--------------------------------------------------------------------------------