├── .gitignore ├── Dockerfile ├── api └── client.http ├── cmd └── walletcore │ └── main.go ├── docker-compose.yaml ├── go.mod ├── go.sum ├── internal ├── database │ ├── account_db.go │ ├── account_db_test.go │ ├── client_db.go │ ├── client_db_test.go │ ├── transaction_db.go │ └── transaction_db_test.go ├── entity │ ├── account.go │ ├── account_test.go │ ├── client.go │ ├── client_test.go │ ├── transaction.go │ └── transaction_test.go ├── event │ ├── balance_updated.go │ ├── handler │ │ ├── balance_updated_kafka.go │ │ └── transaction_created_kafka.go │ └── transaction_created.go ├── gateway │ ├── account.go │ ├── client.go │ └── transaction.go ├── usecase │ ├── create_account │ │ ├── create_account.go │ │ └── create_account_test.go │ ├── create_client │ │ ├── create_client.go │ │ └── create_client_test.go │ ├── create_transaction │ │ ├── create_transaction.go │ │ └── create_transaction_test.go │ └── mocks │ │ ├── account_gateway.go │ │ ├── client_gateway.go │ │ ├── transaction_gateway.go │ │ └── uow.go └── web │ ├── account_handler.go │ ├── client_handler.go │ ├── transaction_handler.go │ └── webserver │ └── webserver.go └── pkg ├── events ├── event_dispatcher.go ├── event_dispatcher_test.go └── interface.go ├── kafka ├── consumer.go ├── producer.go └── producer_test.go └── uow └── uow.go /.gitignore: -------------------------------------------------------------------------------- 1 | .docker -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 2 | 3 | WORKDIR /app/ 4 | 5 | RUN apt-get update && apt-get install -y librdkafka-dev 6 | 7 | CMD ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /api/client.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://localhost:3000/clients HTTP/1.1 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "John Doe", 7 | "email": "john@j.com" 8 | } 9 | 10 | ### 11 | 12 | POST http://localhost:3000/accounts HTTP/1.1 13 | Content-Type: application/json 14 | 15 | { 16 | "client_id": "87495b95-1c7f-4038-ae55-ab36ed6a9411" 17 | } 18 | 19 | ### 20 | 21 | POST http://localhost:8080/transactions HTTP/1.1 22 | Content-Type: application/json 23 | 24 | { 25 | "account_id_from": "f8df753c-3b58-43aa-8016-12aaa4f1ea3e", 26 | "account_id_to": "0216ea38-524f-4e85-8743-d484a8f7538e", 27 | "amount": 1 28 | } -------------------------------------------------------------------------------- /cmd/walletcore/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | "github.com.br/devfullcycle/fc-ms-wallet/internal/database" 9 | "github.com.br/devfullcycle/fc-ms-wallet/internal/event" 10 | "github.com.br/devfullcycle/fc-ms-wallet/internal/event/handler" 11 | createaccount "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/create_account" 12 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/create_client" 13 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/create_transaction" 14 | "github.com.br/devfullcycle/fc-ms-wallet/internal/web" 15 | "github.com.br/devfullcycle/fc-ms-wallet/internal/web/webserver" 16 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/events" 17 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/kafka" 18 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/uow" 19 | ckafka "github.com/confluentinc/confluent-kafka-go/kafka" 20 | _ "github.com/go-sql-driver/mysql" 21 | ) 22 | 23 | func main() { 24 | db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", "root", "root", "mysql", "3306", "wallet")) 25 | if err != nil { 26 | panic(err) 27 | } 28 | defer db.Close() 29 | 30 | configMap := ckafka.ConfigMap{ 31 | "bootstrap.servers": "kafka:29092", 32 | "group.id": "wallet", 33 | } 34 | kafkaProducer := kafka.NewKafkaProducer(&configMap) 35 | 36 | eventDispatcher := events.NewEventDispatcher() 37 | eventDispatcher.Register("TransactionCreated", handler.NewTransactionCreatedKafkaHandler(kafkaProducer)) 38 | eventDispatcher.Register("BalanceUpdated", handler.NewUpdateBalanceKafkaHandler(kafkaProducer)) 39 | transactionCreatedEvent := event.NewTransactionCreated() 40 | balanceUpdatedEvent := event.NewBalanceUpdated() 41 | 42 | clientDb := database.NewClientDB(db) 43 | accountDb := database.NewAccountDB(db) 44 | 45 | ctx := context.Background() 46 | uow := uow.NewUow(ctx, db) 47 | 48 | uow.Register("AccountDB", func(tx *sql.Tx) interface{} { 49 | return database.NewAccountDB(db) 50 | }) 51 | 52 | uow.Register("TransactionDB", func(tx *sql.Tx) interface{} { 53 | return database.NewTransactionDB(db) 54 | }) 55 | createTransactionUseCase := create_transaction.NewCreateTransactionUseCase(uow, eventDispatcher, transactionCreatedEvent, balanceUpdatedEvent) 56 | createClientUseCase := create_client.NewCreateClientUseCase(clientDb) 57 | createAccountUseCase := createaccount.NewCreateAccountUseCase(accountDb, clientDb) 58 | 59 | webserver := webserver.NewWebServer(":8080") 60 | 61 | clientHandler := web.NewWebClientHandler(*createClientUseCase) 62 | accountHandler := web.NewWebAccountHandler(*createAccountUseCase) 63 | transactionHandler := web.NewWebTransactionHandler(*createTransactionUseCase) 64 | 65 | webserver.AddHandler("/clients", clientHandler.CreateClient) 66 | webserver.AddHandler("/accounts", accountHandler.CreateAccount) 67 | webserver.AddHandler("/transactions", transactionHandler.CreateTransaction) 68 | 69 | fmt.Println("Server is running") 70 | webserver.Start() 71 | } 72 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | goapp: 5 | build: . 6 | platform: linux/amd64 7 | volumes: 8 | - .:/app 9 | ports: 10 | - 8080:8080 11 | 12 | mysql: 13 | image: mysql:5.7 14 | platform: linux/amd64 15 | environment: 16 | MYSQL_ROOT_PASSWORD: root 17 | MYSQL_DATABASE: wallet 18 | MYSQL_PASSWORD: root 19 | ports: 20 | - 3306:3306 21 | volumes: 22 | - .docker/mysql:/var/lib/mysql 23 | 24 | zookeeper: 25 | image: "confluentinc/cp-zookeeper:6.1.0" 26 | container_name: zookeeper 27 | ports: 28 | - 2181:2181 29 | environment: 30 | TZ: Sao_Paulo/Brazil 31 | ZOOKEEPER_CLIENT_PORT: 2181 32 | ZOOKEEPER_TICK_TIME: 2000 33 | 34 | kafka: 35 | image: "confluentinc/cp-enterprise-kafka:6.1.0" 36 | container_name: kafka 37 | depends_on: 38 | - zookeeper 39 | ports: 40 | # Exposes 9092 for external connections to the broker 41 | # Use kafka:29092 for connections internal on the docker network 42 | # See https://rmoff.net/2018/08/02/kafka-listeners-explained/ for details 43 | - '9092:9092' 44 | environment: 45 | TZ: Sao_Paulo/Brazil 46 | KAFKA_BROKER_ID: 1 47 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 48 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 49 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 50 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 51 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 52 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 53 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 54 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 55 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 100 56 | CONFLUENT_METRICS_ENABLE: 'false' 57 | 58 | control-center: 59 | image: confluentinc/cp-enterprise-control-center:7.3.0 60 | hostname: control-center 61 | container_name: control-center 62 | depends_on: 63 | - kafka 64 | ports: 65 | - "9021:9021" 66 | environment: 67 | CONTROL_CENTER_BOOTSTRAP_SERVERS: 'kafka:29092' 68 | CONTROL_CENTER_REPLICATION_FACTOR: 1 69 | CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1 70 | CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1 71 | CONFLUENT_METRICS_TOPIC_REPLICATION: 1 72 | PORT: 9021 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com.br/devfullcycle/fc-ms-wallet 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/confluentinc/confluent-kafka-go v1.9.2 7 | github.com/go-chi/chi v1.5.4 8 | github.com/go-chi/chi/v5 v5.0.8 9 | github.com/go-sql-driver/mysql v1.7.0 10 | github.com/google/uuid v1.3.0 11 | github.com/mattn/go-sqlite3 v1.14.15 12 | github.com/stretchr/testify v1.8.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/stretchr/objx v0.4.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= 5 | github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ= 6 | github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= 7 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 9 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 10 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 11 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 12 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 14 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 15 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 16 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 17 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 18 | github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 19 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 20 | github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= 21 | github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo= 22 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 27 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 28 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 29 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 30 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 31 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= 32 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 33 | github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= 34 | github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= 35 | github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= 36 | github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= 37 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 38 | github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= 39 | github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= 40 | github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= 41 | github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 42 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 43 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 44 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 45 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 46 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 48 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 49 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 50 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 51 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 52 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 53 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 54 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 55 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 56 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 57 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 58 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 59 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 60 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 61 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 62 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 63 | github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 64 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 65 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 66 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 68 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 69 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 71 | github.com/google/pprof v0.0.0-20211008130755-947d60d73cc0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= 72 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 73 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 74 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 75 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 76 | github.com/hamba/avro v1.5.6/go.mod h1:3vNT0RLXXpFm2Tb/5KC71ZRJlOroggq1Rcitb6k4Fr8= 77 | github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA= 78 | github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= 79 | github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 80 | github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= 81 | github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= 82 | github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= 83 | github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= 84 | github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= 85 | github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= 86 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 87 | github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= 88 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 89 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 90 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 91 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 92 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 93 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 94 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 95 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 96 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 97 | github.com/linkedin/goavro v2.1.0+incompatible/go.mod h1:bBCwI2eGYpUI/4820s67MElg9tdeLbINjLjiM2xZFYM= 98 | github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= 99 | github.com/linkedin/goavro/v2 v2.10.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= 100 | github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= 101 | github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 102 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 103 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 104 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 105 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 106 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 107 | github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g= 108 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 109 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 111 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 112 | github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= 113 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 114 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 115 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 116 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 117 | github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= 118 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 119 | github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 120 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 121 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 122 | github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 123 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 124 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 125 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 126 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 127 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 128 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 129 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 132 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 133 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 134 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 135 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 136 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 137 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 138 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 140 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 141 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 143 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 144 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 145 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 146 | golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 147 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 148 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 149 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 150 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 151 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 152 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 153 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 154 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 155 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 156 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 160 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 168 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 170 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 171 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 172 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 173 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 174 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 175 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 176 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 177 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 178 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 179 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 180 | golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 181 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 182 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 183 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 185 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 186 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 187 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 188 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 189 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 190 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 191 | google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= 192 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 193 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 194 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 195 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 196 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 197 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 198 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 199 | google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 200 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 201 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 202 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 203 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 204 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 205 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 206 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 207 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 208 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 209 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 210 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 211 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 212 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 213 | gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A= 214 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 215 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 216 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 217 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= 219 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 220 | gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM= 221 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 222 | gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= 223 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 224 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 225 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 226 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 227 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 228 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 229 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 230 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 231 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 232 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 233 | -------------------------------------------------------------------------------- /internal/database/account_db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | ) 8 | 9 | type AccountDB struct { 10 | DB *sql.DB 11 | } 12 | 13 | func NewAccountDB(db *sql.DB) *AccountDB { 14 | return &AccountDB{ 15 | DB: db, 16 | } 17 | } 18 | 19 | func (a *AccountDB) FindByID(id string) (*entity.Account, error) { 20 | var account entity.Account 21 | var client entity.Client 22 | account.Client = &client 23 | 24 | stmt, err := a.DB.Prepare("Select a.id, a.client_id, a.balance, a.created_at, c.id, c.name, c.email, c.created_at FROM accounts a INNER JOIN clients c ON a.client_id = c.id WHERE a.id = ?") 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer stmt.Close() 29 | row := stmt.QueryRow(id) 30 | err = row.Scan( 31 | &account.ID, 32 | &account.Client.ID, 33 | &account.Balance, 34 | &account.CreatedAt, 35 | &client.ID, 36 | &client.Name, 37 | &client.Email, 38 | &client.CreatedAt) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return &account, nil 43 | } 44 | 45 | func (a *AccountDB) Save(account *entity.Account) error { 46 | stmt, err := a.DB.Prepare("INSERT INTO accounts (id, client_id, balance, created_at) VALUES (?, ?, ?, ?)") 47 | if err != nil { 48 | return err 49 | } 50 | defer stmt.Close() 51 | _, err = stmt.Exec(account.ID, account.Client.ID, account.Balance, account.CreatedAt) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func (a *AccountDB) UpdateBalance(account *entity.Account) error { 59 | stmt, err := a.DB.Prepare("UPDATE accounts SET balance = ? WHERE id = ?") 60 | if err != nil { 61 | return err 62 | } 63 | defer stmt.Close() 64 | _, err = stmt.Exec(account.Balance, account.ID) 65 | if err != nil { 66 | return err 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/database/account_db_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type AccountDBTestSuite struct { 12 | suite.Suite 13 | db *sql.DB 14 | accountDB *AccountDB 15 | client *entity.Client 16 | } 17 | 18 | func (s *AccountDBTestSuite) SetupSuite() { 19 | db, err := sql.Open("sqlite3", ":memory:") 20 | s.Nil(err) 21 | s.db = db 22 | db.Exec("Create table clients (id varchar(255), name varchar(255), email varchar(255), created_at date)") 23 | db.Exec("Create table accounts (id varchar(255), client_id varchar(255), balance int, created_at date)") 24 | s.accountDB = NewAccountDB(db) 25 | s.client, _ = entity.NewClient("John", "j@j.com") 26 | } 27 | 28 | func (s *AccountDBTestSuite) TearDownSuite() { 29 | defer s.db.Close() 30 | s.db.Exec("DROP TABLE clients") 31 | s.db.Exec("DROP TABLE accounts") 32 | } 33 | 34 | func TestAccountDBTestSuite(t *testing.T) { 35 | suite.Run(t, new(AccountDBTestSuite)) 36 | } 37 | 38 | func (s *AccountDBTestSuite) TestSave() { 39 | account := entity.NewAccount(s.client) 40 | err := s.accountDB.Save(account) 41 | s.Nil(err) 42 | } 43 | 44 | func (s *AccountDBTestSuite) TestFindByID() { 45 | s.db.Exec("Insert into clients (id, name, email, created_at) values (?, ?, ?, ?)", 46 | s.client.ID, s.client.Name, s.client.Email, s.client.CreatedAt, 47 | ) 48 | account := entity.NewAccount(s.client) 49 | err := s.accountDB.Save(account) 50 | s.Nil(err) 51 | accountDB, err := s.accountDB.FindByID(account.ID) 52 | s.Nil(err) 53 | s.Equal(account.ID, accountDB.ID) 54 | s.Equal(account.ClientID, accountDB.ClientID) 55 | s.Equal(account.Balance, accountDB.Balance) 56 | s.Equal(account.Client.ID, accountDB.Client.ID) 57 | s.Equal(account.Client.Name, accountDB.Client.Name) 58 | s.Equal(account.Client.Email, accountDB.Client.Email) 59 | } -------------------------------------------------------------------------------- /internal/database/client_db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | ) 8 | 9 | type ClientDB struct { 10 | DB *sql.DB 11 | } 12 | 13 | func NewClientDB(db *sql.DB) *ClientDB { 14 | return &ClientDB{ 15 | DB: db, 16 | } 17 | } 18 | 19 | func (c *ClientDB) Get(id string) (*entity.Client, error) { 20 | client := &entity.Client{} 21 | stmt, err := c.DB.Prepare("SELECT id, name, email, created_at FROM clients WHERE id = ?") 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer stmt.Close() 26 | row := stmt.QueryRow(id) 27 | if err := row.Scan(&client.ID, &client.Name, &client.Email, &client.CreatedAt); err != nil { 28 | return nil, err 29 | } 30 | return client, nil 31 | } 32 | 33 | func (c *ClientDB) Save(client *entity.Client) error { 34 | stmt, err := c.DB.Prepare("INSERT INTO clients (id, name, email, created_at) VALUES (?, ?, ?, ?)") 35 | if err != nil { 36 | return err 37 | } 38 | defer stmt.Close() 39 | _, err = stmt.Exec(client.ID, client.Name, client.Email, client.CreatedAt) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } -------------------------------------------------------------------------------- /internal/database/client_db_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 8 | _ "github.com/mattn/go-sqlite3" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | 13 | type ClientDBTestSuite struct { 14 | suite.Suite 15 | db *sql.DB 16 | clientDB *ClientDB 17 | } 18 | 19 | func (s *ClientDBTestSuite) SetupSuite() { 20 | db, err := sql.Open("sqlite3", ":memory:") 21 | s.Nil(err) 22 | s.db = db 23 | db.Exec("Create table clients (id varchar(255), name varchar(255), email varchar(255), created_at date)") 24 | s.clientDB = NewClientDB(db) 25 | } 26 | 27 | func (s *ClientDBTestSuite) TearDownSuite() { 28 | defer s.db.Close() 29 | s.db.Exec("DROP TABLE clients") 30 | } 31 | 32 | func TestClientDBTestSuite(t *testing.T) { 33 | suite.Run(t, new(ClientDBTestSuite)) 34 | } 35 | 36 | func (s *ClientDBTestSuite) TestSave() { 37 | client := &entity.Client{ 38 | ID: "1", 39 | Name: "Test", 40 | Email: "j@j.com", 41 | } 42 | err := s.clientDB.Save(client) 43 | s.Nil(err) 44 | } 45 | 46 | func (s *ClientDBTestSuite) TestGet() { 47 | client, _ := entity.NewClient("John", "j@j.com") 48 | s.clientDB.Save(client) 49 | 50 | clientDB, err := s.clientDB.Get(client.ID) 51 | s.Nil(err) 52 | s.Equal(client.ID, clientDB.ID) 53 | s.Equal(client.Name, clientDB.Name) 54 | s.Equal(client.Email, clientDB.Email) 55 | } -------------------------------------------------------------------------------- /internal/database/transaction_db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | ) 8 | 9 | 10 | type TransactionDB struct { 11 | DB *sql.DB 12 | } 13 | 14 | func NewTransactionDB(db *sql.DB) *TransactionDB { 15 | return &TransactionDB{ 16 | DB: db, 17 | } 18 | } 19 | 20 | func (t *TransactionDB) Create(transaction *entity.Transaction) error { 21 | stmt, err := t.DB.Prepare("INSERT INTO transactions (id, account_id_from, account_id_to, amount, created_at) VALUES (?, ?, ?, ?, ?)") 22 | if err != nil { 23 | return err 24 | } 25 | defer stmt.Close() 26 | _, err = stmt.Exec(transaction.ID, transaction.AccountFrom.ID, transaction.AccountTo.ID, transaction.Amount, transaction.CreatedAt) 27 | if err != nil { 28 | return err 29 | } 30 | return nil 31 | } -------------------------------------------------------------------------------- /internal/database/transaction_db_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | 12 | type TransactionDBTestSuite struct { 13 | suite.Suite 14 | db *sql.DB 15 | client *entity.Client 16 | client2 *entity.Client 17 | accountFrom *entity.Account 18 | accountTo *entity.Account 19 | transactionDB *TransactionDB 20 | } 21 | 22 | func (s *TransactionDBTestSuite) SetupSuite() { 23 | db, err := sql.Open("sqlite3", ":memory:") 24 | s.Nil(err) 25 | s.db = db 26 | db.Exec("Create table clients (id varchar(255), name varchar(255), email varchar(255), created_at date)") 27 | db.Exec("Create table accounts (id varchar(255), client_id varchar(255), balance int, created_at date)") 28 | db.Exec("Create table transactions (id varchar(255), account_id_from varchar(255), account_id_to varchar(255), amount int, created_at date)") 29 | client, err := entity.NewClient("John", "j@j.com") 30 | s.Nil(err) 31 | s.client = client 32 | client2, err := entity.NewClient("John2", "jj@j.com") 33 | s.Nil(err) 34 | s.client2 = client2 35 | //creating accounts 36 | accountFrom := entity.NewAccount(s.client) 37 | accountFrom.Balance = 1000 38 | s.accountFrom = accountFrom 39 | accountTo := entity.NewAccount(s.client2) 40 | accountTo.Balance = 1000 41 | s.accountTo = accountTo 42 | s.transactionDB = NewTransactionDB(db) 43 | } 44 | 45 | func (s *TransactionDBTestSuite) TearDownSuite() { 46 | defer s.db.Close() 47 | s.db.Exec("DROP TABLE clients") 48 | s.db.Exec("DROP TABLE accounts") 49 | s.db.Exec("DROP TABLE transactions") 50 | } 51 | 52 | func TestTransactionDBTestSuite(t *testing.T) { 53 | suite.Run(t, new(TransactionDBTestSuite)) 54 | } 55 | 56 | func (s *TransactionDBTestSuite) TestCreate() { 57 | transaction, err := entity.NewTransaction(s.accountFrom, s.accountTo, 100) 58 | s.Nil(err) 59 | err = s.transactionDB.Create(transaction) 60 | s.Nil(err) 61 | } -------------------------------------------------------------------------------- /internal/entity/account.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Account struct { 10 | ID string 11 | Client *Client 12 | ClientID string 13 | Balance float64 14 | CreatedAt time.Time 15 | UpdatedAt time.Time 16 | } 17 | 18 | func NewAccount(client *Client) *Account { 19 | if client == nil { 20 | return nil 21 | } 22 | account := &Account{ 23 | ID: uuid.New().String(), 24 | Client: client, 25 | Balance: 0, 26 | CreatedAt: time.Now(), 27 | UpdatedAt: time.Now(), 28 | } 29 | return account 30 | } 31 | 32 | func (a *Account) Credit(amount float64) { 33 | a.Balance += amount 34 | a.UpdatedAt = time.Now() 35 | } 36 | 37 | func (a *Account) Debit(amount float64) { 38 | a.Balance -= amount 39 | a.UpdatedAt = time.Now() 40 | } 41 | -------------------------------------------------------------------------------- /internal/entity/account_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCreateAccount(t *testing.T) { 10 | client, _ := NewClient("John Doe", "j@j") 11 | account := NewAccount(client) 12 | assert.NotNil(t, account) 13 | assert.Equal(t, client.ID, account.Client.ID) 14 | } 15 | 16 | func TestCreateAccountWithNilClient(t *testing.T) { 17 | account := NewAccount(nil) 18 | assert.Nil(t, account) 19 | } 20 | 21 | func TestCreditAccount(t *testing.T) { 22 | client, _ := NewClient("John Doe", "j@j") 23 | account := NewAccount(client) 24 | account.Credit(100) 25 | assert.Equal(t, float64(100), account.Balance) 26 | } 27 | 28 | func TestDebitAccount(t *testing.T) { 29 | client, _ := NewClient("John Doe", "j@j") 30 | account := NewAccount(client) 31 | account.Credit(100) 32 | account.Debit(50) 33 | assert.Equal(t, float64(50), account.Balance) 34 | } 35 | -------------------------------------------------------------------------------- /internal/entity/client.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type Client struct { 11 | ID string 12 | Name string 13 | Email string 14 | Accounts []*Account 15 | CreatedAt time.Time 16 | UpdatedAt time.Time 17 | } 18 | 19 | func NewClient(name string, email string) (*Client, error) { 20 | client := &Client{ 21 | ID: uuid.New().String(), 22 | Name: name, 23 | Email: email, 24 | CreatedAt: time.Now(), 25 | UpdatedAt: time.Now(), 26 | } 27 | err := client.Validate() 28 | if err != nil { 29 | return nil, err 30 | } 31 | return client, nil 32 | } 33 | 34 | func (c *Client) Validate() error { 35 | if c.Name == "" { 36 | return errors.New("name is required") 37 | } 38 | if c.Email == "" { 39 | return errors.New("email is required") 40 | } 41 | return nil 42 | } 43 | 44 | func (c *Client) Update(name string, email string) error { 45 | c.Name = name 46 | c.Email = email 47 | c.UpdatedAt = time.Now() 48 | err := c.Validate() 49 | if err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | func (c *Client) AddAccount(account *Account) error { 56 | if account.Client.ID != c.ID { 57 | return errors.New("account does not belong to client") 58 | } 59 | c.Accounts = append(c.Accounts, account) 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/entity/client_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCreateNewClient(t *testing.T) { 10 | client, err := NewClient("John Doe", "j@j.com") 11 | assert.Nil(t, err) 12 | assert.NotNil(t, client) 13 | assert.Equal(t, "John Doe", client.Name) 14 | assert.Equal(t, "j@j.com", client.Email) 15 | } 16 | 17 | func TestCreateNewClientWhenArgsAreInvalid(t *testing.T) { 18 | client, err := NewClient("", "") 19 | assert.NotNil(t, err) 20 | assert.Nil(t, client) 21 | } 22 | 23 | func TestUpdateClient(t *testing.T) { 24 | client, _ := NewClient("John Doe", "j@j.com") 25 | err := client.Update("John Doe Update", "j@j.com") 26 | assert.Nil(t, err) 27 | assert.Equal(t, "John Doe Update", client.Name) 28 | assert.Equal(t, "j@j.com", client.Email) 29 | } 30 | 31 | func TestUpdateClientWithInvalidArgs(t *testing.T) { 32 | client, _ := NewClient("John Doe", "j@j.com") 33 | err := client.Update("", "j@j.com") 34 | assert.Error(t, err, "name is required") 35 | } 36 | 37 | func TestAddAccountToClient(t *testing.T) { 38 | client, _ := NewClient("John Doe", "j@j") 39 | account := NewAccount(client) 40 | err := client.AddAccount(account) 41 | assert.Nil(t, err) 42 | assert.Equal(t, 1, len(client.Accounts)) 43 | } 44 | -------------------------------------------------------------------------------- /internal/entity/transaction.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type Transaction struct { 11 | ID string 12 | AccountFrom *Account `gorm:"foreignKey:AccountFromID"` 13 | AccountFromID string 14 | AccountTo *Account `gorm:"foreignKey:AccountToID"` 15 | AccountToID string 16 | Amount float64 17 | CreatedAt time.Time 18 | } 19 | 20 | func NewTransaction(accountFrom *Account, accountTo *Account, amount float64) (*Transaction, error) { 21 | transaction := &Transaction{ 22 | ID: uuid.New().String(), 23 | AccountFrom: accountFrom, 24 | AccountTo: accountTo, 25 | Amount: amount, 26 | CreatedAt: time.Now(), 27 | } 28 | err := transaction.Validate() 29 | if err != nil { 30 | return nil, err 31 | } 32 | transaction.Commit() 33 | return transaction, nil 34 | } 35 | 36 | func (t *Transaction) Commit() { 37 | t.AccountFrom.Debit(t.Amount) 38 | t.AccountTo.Credit(t.Amount) 39 | } 40 | 41 | func (t *Transaction) Validate() error { 42 | if t.Amount <= 0 { 43 | return errors.New("amount must be greater than zero") 44 | } 45 | if t.AccountFrom.Balance < t.Amount { 46 | return errors.New("insufficient funds") 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/entity/transaction_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCreateTransaction(t *testing.T) { 10 | client1, _ := NewClient("John Doe", "j@j") 11 | account1 := NewAccount(client1) 12 | client2, _ := NewClient("John Doe 2", "j@j2") 13 | account2 := NewAccount(client2) 14 | 15 | account1.Credit(1000) 16 | account2.Credit(1000) 17 | 18 | transaction, err := NewTransaction(account1, account2, 100) 19 | assert.Nil(t, err) 20 | assert.NotNil(t, transaction) 21 | assert.Equal(t, 1100.0, account2.Balance) 22 | assert.Equal(t, 900.0, account1.Balance) 23 | } 24 | 25 | func TestCreateTransactionWithInsuficientBalance(t *testing.T) { 26 | client1, _ := NewClient("John Doe", "j@j") 27 | account1 := NewAccount(client1) 28 | client2, _ := NewClient("John Doe 2", "j@j2") 29 | account2 := NewAccount(client2) 30 | 31 | account1.Credit(1000) 32 | account2.Credit(1000) 33 | 34 | transaction, err := NewTransaction(account1, account2, 2000) 35 | assert.NotNil(t, err) 36 | assert.Error(t, err, "insufficient funds") 37 | assert.Nil(t, transaction) 38 | assert.Equal(t, 1000.0, account2.Balance) 39 | assert.Equal(t, 1000.0, account1.Balance) 40 | } 41 | -------------------------------------------------------------------------------- /internal/event/balance_updated.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "time" 4 | 5 | type BalanceUpdated struct { 6 | Name string 7 | Payload interface{} 8 | } 9 | 10 | func NewBalanceUpdated() *BalanceUpdated { 11 | return &BalanceUpdated{ 12 | Name: "BalanceUpdated", 13 | } 14 | } 15 | 16 | func (e *BalanceUpdated) GetName() string { 17 | return e.Name 18 | } 19 | 20 | func (e *BalanceUpdated) GetPayload() interface{} { 21 | return e.Payload 22 | } 23 | 24 | func (e *BalanceUpdated) SetPayload(payload interface{}) { 25 | e.Payload = payload 26 | } 27 | 28 | func (e *BalanceUpdated) GetDateTime() time.Time { 29 | return time.Now() 30 | } -------------------------------------------------------------------------------- /internal/event/handler/balance_updated_kafka.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/events" 8 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/kafka" 9 | ) 10 | 11 | type UpdateBalanceKafkaHandler struct { 12 | Kafka *kafka.Producer 13 | } 14 | 15 | func NewUpdateBalanceKafkaHandler(kafka *kafka.Producer) *UpdateBalanceKafkaHandler { 16 | return &UpdateBalanceKafkaHandler{ 17 | Kafka: kafka, 18 | } 19 | } 20 | 21 | func (h *UpdateBalanceKafkaHandler) Handle(message events.EventInterface, wg *sync.WaitGroup) { 22 | defer wg.Done() 23 | h.Kafka.Publish(message, nil, "balances") 24 | fmt.Println("UpdateBalanceKafkaHandler called") 25 | } -------------------------------------------------------------------------------- /internal/event/handler/transaction_created_kafka.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/events" 8 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/kafka" 9 | ) 10 | 11 | type TransactionCreatedKafkaHandler struct { 12 | Kafka *kafka.Producer 13 | } 14 | 15 | func NewTransactionCreatedKafkaHandler(kafka *kafka.Producer) *TransactionCreatedKafkaHandler { 16 | return &TransactionCreatedKafkaHandler{ 17 | Kafka: kafka, 18 | } 19 | } 20 | 21 | func (h *TransactionCreatedKafkaHandler) Handle(message events.EventInterface, wg *sync.WaitGroup) { 22 | defer wg.Done() 23 | h.Kafka.Publish(message, nil, "transactions") 24 | fmt.Println("TransactionCreatedKafkaHandler: ", message.GetPayload()) 25 | } 26 | -------------------------------------------------------------------------------- /internal/event/transaction_created.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "time" 4 | 5 | type TransactionCreated struct { 6 | Name string 7 | Payload interface{} 8 | } 9 | 10 | func NewTransactionCreated() *TransactionCreated { 11 | return &TransactionCreated{ 12 | Name: "TransactionCreated", 13 | } 14 | } 15 | 16 | func (e *TransactionCreated) GetName() string { 17 | return e.Name 18 | } 19 | 20 | func (e *TransactionCreated) GetPayload() interface{} { 21 | return e.Payload 22 | } 23 | 24 | func (e *TransactionCreated) SetPayload(payload interface{}) { 25 | e.Payload = payload 26 | } 27 | 28 | func (e *TransactionCreated) GetDateTime() time.Time { 29 | return time.Now() 30 | } 31 | -------------------------------------------------------------------------------- /internal/gateway/account.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 4 | 5 | type AccountGateway interface { 6 | Save(account *entity.Account) error 7 | FindByID(id string) (*entity.Account, error) 8 | UpdateBalance(account *entity.Account) error 9 | } 10 | -------------------------------------------------------------------------------- /internal/gateway/client.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 5 | ) 6 | 7 | type ClientGateway interface { 8 | Get(id string) (*entity.Client, error) 9 | Save(client *entity.Client) error 10 | } 11 | -------------------------------------------------------------------------------- /internal/gateway/transaction.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 4 | 5 | type TransactionGateway interface { 6 | Create(transaction *entity.Transaction) error 7 | } 8 | -------------------------------------------------------------------------------- /internal/usecase/create_account/create_account.go: -------------------------------------------------------------------------------- 1 | package create_account 2 | 3 | import ( 4 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 5 | "github.com.br/devfullcycle/fc-ms-wallet/internal/gateway" 6 | ) 7 | 8 | type CreateAccountInputDTO struct { 9 | ClientID string `json:"client_id"` 10 | } 11 | 12 | type CreateAccountOutputDTO struct { 13 | ID string 14 | } 15 | 16 | type CreateAccountUseCase struct { 17 | AccountGateway gateway.AccountGateway 18 | ClientGateway gateway.ClientGateway 19 | } 20 | 21 | func NewCreateAccountUseCase(a gateway.AccountGateway, c gateway.ClientGateway) *CreateAccountUseCase { 22 | return &CreateAccountUseCase{ 23 | AccountGateway: a, 24 | ClientGateway: c, 25 | } 26 | } 27 | 28 | func (uc *CreateAccountUseCase) Execute(input CreateAccountInputDTO) (*CreateAccountOutputDTO, error) { 29 | client, err := uc.ClientGateway.Get(input.ClientID) 30 | if err != nil { 31 | return nil, err 32 | } 33 | account := entity.NewAccount(client) 34 | err = uc.AccountGateway.Save(account) 35 | if err != nil { 36 | return nil, err 37 | } 38 | output := &CreateAccountOutputDTO{ 39 | ID: account.ID, 40 | } 41 | return output, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/usecase/create_account/create_account_test.go: -------------------------------------------------------------------------------- 1 | package create_account 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/mocks" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | func TestCreateAccountUseCase_Execute(t *testing.T) { 13 | client, _ := entity.NewClient("John Doe", "j@j") 14 | clientMock := &mocks.ClientGatewayMock{} 15 | clientMock.On("Get", client.ID).Return(client, nil) 16 | 17 | accountMock := &mocks.AccountGatewayMock{} 18 | accountMock.On("Save", mock.Anything).Return(nil) 19 | 20 | 21 | uc := NewCreateAccountUseCase(accountMock, clientMock) 22 | inputDto := CreateAccountInputDTO{ 23 | ClientID: client.ID, 24 | } 25 | output, err := uc.Execute(inputDto) 26 | assert.Nil(t, err) 27 | assert.NotNil(t, output.ID) 28 | // asssert valid uuid 29 | clientMock.AssertExpectations(t) 30 | accountMock.AssertExpectations(t) 31 | clientMock.AssertNumberOfCalls(t, "Get", 1) 32 | accountMock.AssertNumberOfCalls(t, "Save", 1) 33 | } 34 | -------------------------------------------------------------------------------- /internal/usecase/create_client/create_client.go: -------------------------------------------------------------------------------- 1 | package create_client 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/gateway" 8 | ) 9 | 10 | type CreateClientInputDTO struct { 11 | Name string 12 | Email string 13 | } 14 | 15 | type CreateClientOutputDTO struct { 16 | ID string 17 | Name string 18 | Email string 19 | CreatedAt time.Time 20 | UpdatedAt time.Time 21 | } 22 | 23 | type CreateClientUseCase struct { 24 | ClientGateway gateway.ClientGateway 25 | } 26 | 27 | func NewCreateClientUseCase(clientGateway gateway.ClientGateway) *CreateClientUseCase { 28 | return &CreateClientUseCase{ 29 | ClientGateway: clientGateway, 30 | } 31 | } 32 | 33 | func (uc *CreateClientUseCase) Execute(input CreateClientInputDTO) (*CreateClientOutputDTO, error) { 34 | client, err := entity.NewClient(input.Name, input.Email) 35 | if err != nil { 36 | return nil, err 37 | } 38 | err = uc.ClientGateway.Save(client) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | output := &CreateClientOutputDTO{ 44 | ID: client.ID, 45 | Name: client.Name, 46 | Email: client.Email, 47 | CreatedAt: client.CreatedAt, 48 | UpdatedAt: client.UpdatedAt, 49 | } 50 | return output, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/usecase/create_client/create_client_test.go: -------------------------------------------------------------------------------- 1 | package create_client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | type ClientGatewayMock struct { 12 | mock.Mock 13 | } 14 | 15 | func (m *ClientGatewayMock) Save(client *entity.Client) error { 16 | args := m.Called(client) 17 | return args.Error(0) 18 | } 19 | 20 | func (m *ClientGatewayMock) Get(id string) (*entity.Client, error) { 21 | args := m.Called(id) 22 | return args.Get(0).(*entity.Client), args.Error(1) 23 | } 24 | 25 | func TestCreateClientUseCase_Execute(t *testing.T) { 26 | m := &ClientGatewayMock{} 27 | m.On("Save", mock.Anything).Return(nil) 28 | uc := NewCreateClientUseCase(m) 29 | 30 | output, err := uc.Execute(CreateClientInputDTO{ 31 | Name: "John Doe", 32 | Email: "j@j", 33 | }) 34 | assert.Nil(t, err) 35 | assert.NotNil(t, output) 36 | assert.NotEmpty(t, output.ID) 37 | assert.Equal(t, "John Doe", output.Name) 38 | assert.Equal(t, "j@j", output.Email) 39 | m.AssertExpectations(t) 40 | m.AssertNumberOfCalls(t, "Save", 1) 41 | } 42 | -------------------------------------------------------------------------------- /internal/usecase/create_transaction/create_transaction.go: -------------------------------------------------------------------------------- 1 | package create_transaction 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/gateway" 8 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/events" 9 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/uow" 10 | ) 11 | 12 | type CreateTransactionInputDTO struct { 13 | AccountIDFrom string `json:"account_id_from"` 14 | AccountIDTo string `json:"account_id_to"` 15 | Amount float64 `json:"amount"` 16 | } 17 | 18 | type CreateTransactionOutputDTO struct { 19 | ID string `json:"id"` 20 | AccountIDFrom string `json:"account_id_from"` 21 | AccountIDTo string `json:"account_id_to"` 22 | Amount float64 `json:"amount"` 23 | } 24 | 25 | type BalanceUpdatedOutputDTO struct { 26 | AccountIDFrom string `json:"account_id_from"` 27 | AccountIDTo string `json:"account_id_to"` 28 | BalanceAccountIDFrom float64 `json:"balance_account_id_from"` 29 | BalanceAccountIDTo float64 `json:"balance_account_id_to"` 30 | } 31 | 32 | type CreateTransactionUseCase struct { 33 | Uow uow.UowInterface 34 | EventDispatcher events.EventDispatcherInterface 35 | TransactionCreated events.EventInterface 36 | BalanceUpdated events.EventInterface 37 | } 38 | 39 | func NewCreateTransactionUseCase( 40 | Uow uow.UowInterface, 41 | eventDispatcher events.EventDispatcherInterface, 42 | transactionCreated events.EventInterface, 43 | balanceUpdated events.EventInterface, 44 | ) *CreateTransactionUseCase { 45 | return &CreateTransactionUseCase{ 46 | Uow: Uow, 47 | EventDispatcher: eventDispatcher, 48 | TransactionCreated: transactionCreated, 49 | BalanceUpdated: balanceUpdated, 50 | } 51 | } 52 | 53 | func (uc *CreateTransactionUseCase) Execute(ctx context.Context, input CreateTransactionInputDTO) (*CreateTransactionOutputDTO, error) { 54 | output := &CreateTransactionOutputDTO{} 55 | balanceUpdatedOutput := &BalanceUpdatedOutputDTO{} 56 | err := uc.Uow.Do(ctx, func(_ *uow.Uow) error { 57 | accountRepository := uc.getAccountRepository(ctx) 58 | transactionRepository := uc.getTransactionRepository(ctx) 59 | 60 | accountFrom, err := accountRepository.FindByID(input.AccountIDFrom) 61 | if err != nil { 62 | return err 63 | } 64 | accountTo, err := accountRepository.FindByID(input.AccountIDTo) 65 | if err != nil { 66 | return err 67 | } 68 | transaction, err := entity.NewTransaction(accountFrom, accountTo, input.Amount) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | err = accountRepository.UpdateBalance(accountFrom) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | err = accountRepository.UpdateBalance(accountTo) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | err = transactionRepository.Create(transaction) 84 | if err != nil { 85 | return err 86 | } 87 | output.ID = transaction.ID 88 | output.AccountIDFrom = input.AccountIDFrom 89 | output.AccountIDTo = input.AccountIDTo 90 | output.Amount = input.Amount 91 | 92 | balanceUpdatedOutput.AccountIDFrom = input.AccountIDFrom 93 | balanceUpdatedOutput.AccountIDTo = input.AccountIDTo 94 | balanceUpdatedOutput.BalanceAccountIDFrom = accountFrom.Balance 95 | balanceUpdatedOutput.BalanceAccountIDTo = accountTo.Balance 96 | return nil 97 | }) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | uc.TransactionCreated.SetPayload(output) 103 | uc.EventDispatcher.Dispatch(uc.TransactionCreated) 104 | 105 | uc.BalanceUpdated.SetPayload(balanceUpdatedOutput) 106 | uc.EventDispatcher.Dispatch(uc.BalanceUpdated) 107 | return output, nil 108 | } 109 | 110 | func (uc *CreateTransactionUseCase) getAccountRepository(ctx context.Context) gateway.AccountGateway { 111 | repo, err := uc.Uow.GetRepository(ctx, "AccountDB") 112 | if err != nil { 113 | panic(err) 114 | } 115 | return repo.(gateway.AccountGateway) 116 | } 117 | 118 | func (uc *CreateTransactionUseCase) getTransactionRepository(ctx context.Context) gateway.TransactionGateway { 119 | repo, err := uc.Uow.GetRepository(ctx, "TransactionDB") 120 | if err != nil { 121 | panic(err) 122 | } 123 | return repo.(gateway.TransactionGateway) 124 | } 125 | -------------------------------------------------------------------------------- /internal/usecase/create_transaction/create_transaction_test.go: -------------------------------------------------------------------------------- 1 | package create_transaction 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 8 | "github.com.br/devfullcycle/fc-ms-wallet/internal/event" 9 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/mocks" 10 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/events" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | func TestCreateTransactionUseCase_Execute(t *testing.T) { 16 | client1, _ := entity.NewClient("client1", "j@j.com") 17 | account1 := entity.NewAccount(client1) 18 | account1.Credit(1000) 19 | 20 | client2, _ := entity.NewClient("client2", "j@j2.com") 21 | account2 := entity.NewAccount(client2) 22 | account2.Credit(1000) 23 | 24 | mockUow := &mocks.UowMock{} 25 | mockUow.On("Do", mock.Anything, mock.Anything).Return(nil) 26 | 27 | inputDto := CreateTransactionInputDTO{ 28 | AccountIDFrom: account1.ID, 29 | AccountIDTo: account2.ID, 30 | Amount: 100, 31 | } 32 | 33 | dispatcher := events.NewEventDispatcher() 34 | eventTransaction := event.NewTransactionCreated() 35 | eventBalance := event.NewBalanceUpdated() 36 | ctx := context.Background() 37 | 38 | uc := NewCreateTransactionUseCase(mockUow, dispatcher, eventTransaction, eventBalance) 39 | output, err := uc.Execute(ctx, inputDto) 40 | assert.Nil(t, err) 41 | assert.NotNil(t, output) 42 | mockUow.AssertExpectations(t) 43 | mockUow.AssertNumberOfCalls(t, "Do", 1) 44 | } 45 | -------------------------------------------------------------------------------- /internal/usecase/mocks/account_gateway.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type AccountGatewayMock struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *AccountGatewayMock) Save(account *entity.Account) error { 13 | args := m.Called(account) 14 | return args.Error(0) 15 | } 16 | 17 | func (m *AccountGatewayMock) FindByID(id string) (*entity.Account, error) { 18 | args := m.Called(id) 19 | return args.Get(0).(*entity.Account), args.Error(1) 20 | } 21 | 22 | func (m *AccountGatewayMock) UpdateBalance(account *entity.Account) error { 23 | args := m.Called(account) 24 | return args.Error(0) 25 | } 26 | -------------------------------------------------------------------------------- /internal/usecase/mocks/client_gateway.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type ClientGatewayMock struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *ClientGatewayMock) Save(client *entity.Client) error { 13 | args := m.Called(client) 14 | return args.Error(0) 15 | } 16 | 17 | func (m *ClientGatewayMock) Get(id string) (*entity.Client, error) { 18 | args := m.Called(id) 19 | return args.Get(0).(*entity.Client), args.Error(1) 20 | } 21 | -------------------------------------------------------------------------------- /internal/usecase/mocks/transaction_gateway.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com.br/devfullcycle/fc-ms-wallet/internal/entity" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type TransactionGatewayMock struct { 9 | mock.Mock 10 | } 11 | 12 | func (m *TransactionGatewayMock) Create(transaction *entity.Transaction) error { 13 | args := m.Called(transaction) 14 | return args.Error(0) 15 | } 16 | -------------------------------------------------------------------------------- /internal/usecase/mocks/uow.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com.br/devfullcycle/fc-ms-wallet/pkg/uow" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type UowMock struct { 11 | mock.Mock 12 | } 13 | 14 | // Register(name string, fc RepositoryFactory) 15 | // GetRepository(ctx context.Context, name string) (interface{}, error) 16 | // Do(ctx context.Context, fn func(uow *Uow) error) error 17 | // CommitOrRollback() error 18 | // Rollback() error 19 | // UnRegister(name string) 20 | 21 | func (m *UowMock) Register(name string, fc uow.RepositoryFactory) { 22 | m.Called(name, fc) 23 | } 24 | 25 | func (m *UowMock) GetRepository(ctx context.Context, name string) (interface{}, error) { 26 | args := m.Called(name) 27 | return args.Get(0), args.Error(1) 28 | } 29 | 30 | func (m *UowMock) Do(ctx context.Context, fn func(uow *uow.Uow) error) error { 31 | args := m.Called(fn) 32 | return args.Error(0) 33 | } 34 | 35 | func (m *UowMock) CommitOrRollback() error { 36 | args := m.Called() 37 | return args.Error(0) 38 | } 39 | 40 | func (m *UowMock) Rollback() error { 41 | args := m.Called() 42 | return args.Error(0) 43 | } 44 | 45 | func (m *UowMock) UnRegister(name string) { 46 | m.Called(name) 47 | } 48 | -------------------------------------------------------------------------------- /internal/web/account_handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/create_account" 9 | ) 10 | 11 | type WebAccountHandler struct { 12 | CreateAccountUseCase create_account.CreateAccountUseCase 13 | } 14 | 15 | func NewWebAccountHandler(createAccountUseCase create_account.CreateAccountUseCase) *WebAccountHandler { 16 | return &WebAccountHandler{ 17 | CreateAccountUseCase: createAccountUseCase, 18 | } 19 | } 20 | 21 | func (h *WebAccountHandler) CreateAccount(w http.ResponseWriter, r *http.Request) { 22 | var dto create_account.CreateAccountInputDTO 23 | err := json.NewDecoder(r.Body).Decode(&dto) 24 | if err != nil { 25 | w.WriteHeader(http.StatusBadRequest) 26 | fmt.Println(err) 27 | return 28 | } 29 | 30 | output, err := h.CreateAccountUseCase.Execute(dto) 31 | if err != nil { 32 | w.WriteHeader(http.StatusInternalServerError) 33 | fmt.Println(err) 34 | return 35 | } 36 | 37 | w.Header().Set("Content-Type", "application/json") 38 | err = json.NewEncoder(w).Encode(output) 39 | if err != nil { 40 | w.WriteHeader(http.StatusInternalServerError) 41 | return 42 | } 43 | w.WriteHeader(http.StatusCreated) 44 | } -------------------------------------------------------------------------------- /internal/web/client_handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/create_client" 8 | ) 9 | 10 | type WebClientHandler struct { 11 | CreateClientUseCase create_client.CreateClientUseCase 12 | } 13 | 14 | func NewWebClientHandler(createClientUseCase create_client.CreateClientUseCase) *WebClientHandler { 15 | return &WebClientHandler{ 16 | CreateClientUseCase: createClientUseCase, 17 | } 18 | } 19 | 20 | func (h *WebClientHandler) CreateClient(w http.ResponseWriter, r *http.Request) { 21 | var dto create_client.CreateClientInputDTO 22 | err := json.NewDecoder(r.Body).Decode(&dto) 23 | if err != nil { 24 | w.WriteHeader(http.StatusBadRequest) 25 | return 26 | } 27 | 28 | output, err := h.CreateClientUseCase.Execute(dto) 29 | if err != nil { 30 | w.WriteHeader(http.StatusInternalServerError) 31 | return 32 | } 33 | 34 | w.Header().Set("Content-Type", "application/json") 35 | err = json.NewEncoder(w).Encode(output) 36 | if err != nil { 37 | w.WriteHeader(http.StatusInternalServerError) 38 | return 39 | } 40 | w.WriteHeader(http.StatusCreated) 41 | } -------------------------------------------------------------------------------- /internal/web/transaction_handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com.br/devfullcycle/fc-ms-wallet/internal/usecase/create_transaction" 8 | ) 9 | 10 | type WebTransactionHandler struct { 11 | CreateTransactionUseCase create_transaction.CreateTransactionUseCase 12 | } 13 | 14 | func NewWebTransactionHandler(createTransactionUseCase create_transaction.CreateTransactionUseCase) *WebTransactionHandler { 15 | return &WebTransactionHandler{ 16 | CreateTransactionUseCase: createTransactionUseCase, 17 | } 18 | } 19 | 20 | func (h *WebTransactionHandler) CreateTransaction(w http.ResponseWriter, r *http.Request) { 21 | var dto create_transaction.CreateTransactionInputDTO 22 | err := json.NewDecoder(r.Body).Decode(&dto) 23 | if err != nil { 24 | w.WriteHeader(http.StatusBadRequest) 25 | return 26 | } 27 | ctx := r.Context() 28 | output, err := h.CreateTransactionUseCase.Execute(ctx, dto) 29 | if err != nil { 30 | w.WriteHeader(http.StatusBadRequest) 31 | w.Write([]byte(err.Error())) 32 | return 33 | } 34 | 35 | w.Header().Set("Content-Type", "application/json") 36 | err = json.NewEncoder(w).Encode(output) 37 | if err != nil { 38 | w.WriteHeader(http.StatusInternalServerError) 39 | return 40 | } 41 | w.WriteHeader(http.StatusCreated) 42 | } 43 | -------------------------------------------------------------------------------- /internal/web/webserver/webserver.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/middleware" 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | type WebServer struct { 11 | Router chi.Router 12 | Handlers map[string]http.HandlerFunc 13 | WebServerPort string 14 | } 15 | 16 | func NewWebServer(webServerPort string) *WebServer { 17 | return &WebServer{ 18 | Router: chi.NewRouter(), 19 | Handlers: make(map[string]http.HandlerFunc), 20 | WebServerPort: webServerPort, 21 | } 22 | } 23 | 24 | func (s *WebServer) AddHandler(path string, handler http.HandlerFunc) { 25 | s.Handlers[path] = handler 26 | } 27 | 28 | func (s *WebServer) Start() { 29 | s.Router.Use(middleware.Logger) 30 | for path, handler := range s.Handlers { 31 | s.Router.Post(path, handler) 32 | } 33 | http.ListenAndServe(s.WebServerPort, s.Router) 34 | } -------------------------------------------------------------------------------- /pkg/events/event_dispatcher.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var ErrHandlerAlreadyRegistered = errors.New("handler already registered") 9 | 10 | type EventDispatcher struct { 11 | handlers map[string][]EventHandlerInterface 12 | } 13 | 14 | func NewEventDispatcher() *EventDispatcher { 15 | return &EventDispatcher{ 16 | handlers: make(map[string][]EventHandlerInterface), 17 | } 18 | } 19 | 20 | func (ev *EventDispatcher) Dispatch(event EventInterface) error { 21 | if handlers, ok := ev.handlers[event.GetName()]; ok { 22 | wg := &sync.WaitGroup{} 23 | for _, handler := range handlers { 24 | wg.Add(1) 25 | go handler.Handle(event, wg) 26 | } 27 | wg.Wait() 28 | } 29 | return nil 30 | } 31 | 32 | func (ed *EventDispatcher) Register(eventName string, handler EventHandlerInterface) error { 33 | if _, ok := ed.handlers[eventName]; ok { 34 | for _, h := range ed.handlers[eventName] { 35 | if h == handler { 36 | return ErrHandlerAlreadyRegistered 37 | } 38 | } 39 | } 40 | ed.handlers[eventName] = append(ed.handlers[eventName], handler) 41 | return nil 42 | } 43 | 44 | func (ed *EventDispatcher) Has(eventName string, handler EventHandlerInterface) bool { 45 | if _, ok := ed.handlers[eventName]; ok { 46 | for _, h := range ed.handlers[eventName] { 47 | if h == handler { 48 | return true 49 | } 50 | } 51 | } 52 | return false 53 | } 54 | 55 | func (ed *EventDispatcher) Remove(eventName string, handler EventHandlerInterface) error { 56 | if _, ok := ed.handlers[eventName]; ok { 57 | for i, h := range ed.handlers[eventName] { 58 | if h == handler { 59 | ed.handlers[eventName] = append(ed.handlers[eventName][:i], ed.handlers[eventName][i+1:]...) 60 | return nil 61 | } 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func (ed *EventDispatcher) Clear() { 68 | ed.handlers = make(map[string][]EventHandlerInterface) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/events/event_dispatcher_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type TestEvent struct { 14 | Name string 15 | Payload interface{} 16 | } 17 | 18 | func (e *TestEvent) GetName() string { 19 | return e.Name 20 | } 21 | 22 | func (e *TestEvent) GetPayload() interface{} { 23 | return e.Payload 24 | } 25 | 26 | func (e *TestEvent) GetDateTime() time.Time { 27 | return time.Now() 28 | } 29 | 30 | func (e *TestEvent) SetPayload(payload interface{}) { 31 | e.Payload = payload 32 | } 33 | 34 | type TestEventHandler struct { 35 | ID int 36 | } 37 | 38 | func (h *TestEventHandler) Handle(event EventInterface, wg *sync.WaitGroup) { 39 | } 40 | 41 | type EventDispatcherTestSuite struct { 42 | suite.Suite 43 | event TestEvent 44 | event2 TestEvent 45 | handler TestEventHandler 46 | handler2 TestEventHandler 47 | handler3 TestEventHandler 48 | eventDispatcher *EventDispatcher 49 | } 50 | 51 | func (suite *EventDispatcherTestSuite) SetupTest() { 52 | suite.eventDispatcher = NewEventDispatcher() 53 | suite.handler = TestEventHandler{ 54 | ID: 1, 55 | } 56 | suite.handler2 = TestEventHandler{ 57 | ID: 2, 58 | } 59 | suite.handler3 = TestEventHandler{ 60 | ID: 3, 61 | } 62 | suite.event = TestEvent{Name: "test", Payload: "test"} 63 | suite.event2 = TestEvent{Name: "test2", Payload: "test2"} 64 | } 65 | 66 | func (suite *EventDispatcherTestSuite) TestEventDispatcher_Register() { 67 | err := suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler) 68 | suite.Nil(err) 69 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 70 | 71 | err = suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler2) 72 | suite.Nil(err) 73 | suite.Equal(2, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 74 | 75 | assert.Equal(suite.T(), &suite.handler, suite.eventDispatcher.handlers[suite.event.GetName()][0]) 76 | assert.Equal(suite.T(), &suite.handler2, suite.eventDispatcher.handlers[suite.event.GetName()][1]) 77 | } 78 | 79 | func (suite *EventDispatcherTestSuite) TestEventDispatcher_Register_WithSameHandler() { 80 | err := suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler) 81 | suite.Nil(err) 82 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 83 | 84 | err = suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler) 85 | suite.Equal(ErrHandlerAlreadyRegistered, err) 86 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 87 | } 88 | 89 | func (suite *EventDispatcherTestSuite) TestEventDispatcher_Clear() { 90 | // Event 1 91 | err := suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler) 92 | suite.Nil(err) 93 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 94 | 95 | err = suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler2) 96 | suite.Nil(err) 97 | suite.Equal(2, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 98 | 99 | // Event 2 100 | err = suite.eventDispatcher.Register(suite.event2.GetName(), &suite.handler3) 101 | suite.Nil(err) 102 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event2.GetName()])) 103 | 104 | suite.eventDispatcher.Clear() 105 | suite.Equal(0, len(suite.eventDispatcher.handlers)) 106 | } 107 | 108 | func (suite *EventDispatcherTestSuite) TestEventDispatcher_Has() { 109 | // Event 1 110 | err := suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler) 111 | suite.Nil(err) 112 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 113 | 114 | err = suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler2) 115 | suite.Nil(err) 116 | suite.Equal(2, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 117 | 118 | assert.True(suite.T(), suite.eventDispatcher.Has(suite.event.GetName(), &suite.handler)) 119 | assert.True(suite.T(), suite.eventDispatcher.Has(suite.event.GetName(), &suite.handler2)) 120 | assert.False(suite.T(), suite.eventDispatcher.Has(suite.event.GetName(), &suite.handler3)) 121 | } 122 | 123 | func (suite *EventDispatcherTestSuite) TestEventDispatcher_Remove() { 124 | // Event 1 125 | err := suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler) 126 | suite.Nil(err) 127 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 128 | 129 | err = suite.eventDispatcher.Register(suite.event.GetName(), &suite.handler2) 130 | suite.Nil(err) 131 | suite.Equal(2, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 132 | 133 | // Event 2 134 | err = suite.eventDispatcher.Register(suite.event2.GetName(), &suite.handler3) 135 | suite.Nil(err) 136 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event2.GetName()])) 137 | 138 | suite.eventDispatcher.Remove(suite.event.GetName(), &suite.handler) 139 | suite.Equal(1, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 140 | assert.Equal(suite.T(), &suite.handler2, suite.eventDispatcher.handlers[suite.event.GetName()][0]) 141 | 142 | suite.eventDispatcher.Remove(suite.event.GetName(), &suite.handler2) 143 | suite.Equal(0, len(suite.eventDispatcher.handlers[suite.event.GetName()])) 144 | 145 | suite.eventDispatcher.Remove(suite.event2.GetName(), &suite.handler3) 146 | suite.Equal(0, len(suite.eventDispatcher.handlers[suite.event2.GetName()])) 147 | } 148 | 149 | type MockHandler struct { 150 | mock.Mock 151 | } 152 | 153 | func (m *MockHandler) Handle(event EventInterface, wg *sync.WaitGroup) { 154 | m.Called(event) 155 | wg.Done() 156 | } 157 | 158 | func (suite *EventDispatcherTestSuite) TestEventDispatch_Dispatch() { 159 | eh := &MockHandler{} 160 | eh.On("Handle", &suite.event) 161 | 162 | eh2 := &MockHandler{} 163 | eh2.On("Handle", &suite.event) 164 | 165 | suite.eventDispatcher.Register(suite.event.GetName(), eh) 166 | suite.eventDispatcher.Register(suite.event.GetName(), eh2) 167 | 168 | suite.eventDispatcher.Dispatch(&suite.event) 169 | eh.AssertExpectations(suite.T()) 170 | eh2.AssertExpectations(suite.T()) 171 | eh.AssertNumberOfCalls(suite.T(), "Handle", 1) 172 | eh2.AssertNumberOfCalls(suite.T(), "Handle", 1) 173 | } 174 | 175 | func TestSuite(t *testing.T) { 176 | suite.Run(t, new(EventDispatcherTestSuite)) 177 | } 178 | -------------------------------------------------------------------------------- /pkg/events/interface.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type EventInterface interface { 9 | GetName() string 10 | GetDateTime() time.Time 11 | GetPayload() interface{} 12 | SetPayload(payload interface{}) 13 | } 14 | 15 | type EventHandlerInterface interface { 16 | Handle(event EventInterface, wg *sync.WaitGroup) 17 | } 18 | 19 | type EventDispatcherInterface interface { 20 | Register(eventName string, handler EventHandlerInterface) error 21 | Dispatch(event EventInterface) error 22 | Remove(eventName string, handler EventHandlerInterface) error 23 | Has(eventName string, handler EventHandlerInterface) bool 24 | Clear() 25 | } 26 | -------------------------------------------------------------------------------- /pkg/kafka/consumer.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ckafka "github.com/confluentinc/confluent-kafka-go/kafka" 4 | 5 | type Consumer struct { 6 | ConfigMap *ckafka.ConfigMap 7 | Topics []string 8 | } 9 | 10 | func NewConsumer(configMap *ckafka.ConfigMap, topics []string) *Consumer { 11 | return &Consumer{ 12 | ConfigMap: configMap, 13 | Topics: topics, 14 | } 15 | } 16 | 17 | func (c *Consumer) Consume(msgChan chan *ckafka.Message) error { 18 | consumer, err := ckafka.NewConsumer(c.ConfigMap) 19 | if err != nil { 20 | panic(err) 21 | } 22 | err = consumer.SubscribeTopics(c.Topics, nil) 23 | if err != nil { 24 | panic(err) 25 | } 26 | for { 27 | msg, err := consumer.ReadMessage(-1) 28 | if err == nil { 29 | msgChan <- msg 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/kafka/producer.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | ckafka "github.com/confluentinc/confluent-kafka-go/kafka" 7 | ) 8 | 9 | type Producer struct { 10 | ConfigMap *ckafka.ConfigMap 11 | } 12 | 13 | func NewKafkaProducer(configMap *ckafka.ConfigMap) *Producer { 14 | return &Producer{ConfigMap: configMap} 15 | } 16 | 17 | func (p *Producer) Publish(msg interface{}, key []byte, topic string) error { 18 | producer, err := ckafka.NewProducer(p.ConfigMap) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | msgJson, err := json.Marshal(msg) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | message := &ckafka.Message{ 29 | TopicPartition: ckafka.TopicPartition{Topic: &topic, Partition: ckafka.PartitionAny}, 30 | Value: msgJson, 31 | Key: key, 32 | } 33 | err = producer.Produce(message, nil) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/kafka/producer_test.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "testing" 5 | 6 | ckafka "github.com/confluentinc/confluent-kafka-go/kafka" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestProducerPublish(t *testing.T) { 11 | type TransactionDtoOutput struct { 12 | ID string `json:"id"` 13 | Status string `json:"status"` 14 | ErrorMessage string `json:"error_message"` 15 | } 16 | 17 | expectedOutput := TransactionDtoOutput{ 18 | ID: "1", 19 | Status: "rejected", 20 | ErrorMessage: "you dont have limit for this transaction", 21 | } 22 | // outputJson, _ := json.Marshal(expectedOutput) 23 | 24 | configMap := ckafka.ConfigMap{ 25 | "test.mock.num.brokers": 3, 26 | } 27 | producer := NewKafkaProducer(&configMap) 28 | err := producer.Publish(expectedOutput, []byte("1"), "test") 29 | assert.Nil(t, err) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/uow/uow.go: -------------------------------------------------------------------------------- 1 | package uow 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | type RepositoryFactory func(tx *sql.Tx) interface{} 11 | 12 | type UowInterface interface { 13 | Register(name string, fc RepositoryFactory) 14 | GetRepository(ctx context.Context, name string) (interface{}, error) 15 | Do(ctx context.Context, fn func(uow *Uow) error) error 16 | CommitOrRollback() error 17 | Rollback() error 18 | UnRegister(name string) 19 | } 20 | 21 | type Uow struct { 22 | Db *sql.DB 23 | Tx *sql.Tx 24 | Repositories map[string]RepositoryFactory 25 | } 26 | 27 | func NewUow(ctx context.Context, db *sql.DB) *Uow { 28 | return &Uow{ 29 | Db: db, 30 | Repositories: make(map[string]RepositoryFactory), 31 | } 32 | } 33 | 34 | func (u *Uow) Register(name string, fc RepositoryFactory) { 35 | u.Repositories[name] = fc 36 | } 37 | 38 | func (u *Uow) UnRegister(name string) { 39 | delete(u.Repositories, name) 40 | } 41 | 42 | func (u *Uow) GetRepository(ctx context.Context, name string) (interface{}, error) { 43 | if u.Tx == nil { 44 | tx, err := u.Db.BeginTx(ctx, nil) 45 | if err != nil { 46 | return nil, err 47 | } 48 | u.Tx = tx 49 | } 50 | repo := u.Repositories[name](u.Tx) 51 | return repo, nil 52 | } 53 | 54 | func (u *Uow) Do(ctx context.Context, fn func(Uow *Uow) error) error { 55 | if u.Tx != nil { 56 | return fmt.Errorf("transaction already started") 57 | } 58 | tx, err := u.Db.BeginTx(ctx, nil) 59 | if err != nil { 60 | return err 61 | } 62 | u.Tx = tx 63 | err = fn(u) 64 | if err != nil { 65 | errRb := u.Rollback() 66 | if errRb != nil { 67 | return errors.New(fmt.Sprintf("original error: %s, rollback error: %s", err.Error(), errRb.Error())) 68 | } 69 | return err 70 | } 71 | return u.CommitOrRollback() 72 | } 73 | 74 | func (u *Uow) Rollback() error { 75 | if u.Tx == nil { 76 | return errors.New("no transaction to rollback") 77 | } 78 | err := u.Tx.Rollback() 79 | if err != nil { 80 | return err 81 | } 82 | u.Tx = nil 83 | return nil 84 | } 85 | 86 | func (u *Uow) CommitOrRollback() error { 87 | err := u.Tx.Commit() 88 | if err != nil { 89 | errRb := u.Rollback() 90 | if errRb != nil { 91 | return errors.New(fmt.Sprintf("original error: %s, rollback error: %s", err.Error(), errRb.Error())) 92 | } 93 | return err 94 | } 95 | u.Tx = nil 96 | return nil 97 | } 98 | --------------------------------------------------------------------------------