├── .gitignore ├── .golangci.yml ├── Makefile ├── README.md ├── cmd ├── config │ └── config.go ├── cron │ └── outbox_procuder │ │ └── order_producer.go └── main.go ├── conf.yaml ├── docker-compose.yml ├── go.mod ├── go.sum ├── handler ├── create_order │ ├── create.go │ ├── create_test.go │ ├── domain.go │ └── validate.go ├── echo │ └── handler.go └── get_orders │ ├── get.go │ └── get_test.go ├── internal ├── cron │ └── outbox_producer │ │ └── outbox_producer.go ├── kafka │ └── domain.go ├── pkg │ ├── entity │ │ └── order │ │ │ └── order.go │ └── repository │ │ └── order │ │ ├── fake_order_repo │ │ └── repository.go │ │ ├── mocks │ │ └── mock_repository.go │ │ ├── mocks2 │ │ └── repo_mock.go │ │ └── repository.go └── usecase │ └── order │ └── order.go ├── logger └── logger.go └── migration ├── 01_create_orders.sql └── 02_outbox_table.sql /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | vendor -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | build-tags: 3 | - integration 4 | linters-settings: 5 | gocyclo: 6 | min-complexity: 30 7 | gocritic: 8 | enabled-tags: 9 | - diagnostic 10 | - experimental 11 | - opinionated 12 | - style 13 | disabled-checks: 14 | - dupImport # https://github.com/go-critic/go-critic/issues/845 15 | - ifElseChain 16 | - octalLiteral 17 | - paramTypeCombine 18 | - sloppyReassign 19 | - unnamedResult 20 | - whyNoLint 21 | - wrapperFunc 22 | goimports: 23 | local-prefixes: go.avito.ru/av/service-tariff 24 | govet: 25 | check-shadowing: true 26 | golint: 27 | min-confidence: 0.8 28 | lll: 29 | line-length: 140 30 | staticcheck: 31 | # https://staticcheck.io/docs/options#checks 32 | checks: ["all"] 33 | wrapcheck: 34 | ignoreSigs: 35 | - .Errorf( 36 | - errors.New( 37 | - errors.Unwrap( 38 | - .ProcessSequence( 39 | - .Wrap( 40 | - .Wrapf( 41 | - .WithMessage( 42 | - .WithMessagef( 43 | - .WithStack( 44 | wsl: 45 | # See https://github.com/bombsimon/wsl/blob/master/doc/configuration.md for 46 | # documentation of available settings. These are the defaults for 47 | # `golangci-lint`. 48 | allow-assign-and-anything: true 49 | allow-assign-and-call: true 50 | allow-cuddle-declarations: true 51 | allow-trailing-comment: true 52 | linters: 53 | enable: 54 | - gocyclo 55 | - asciicheck 56 | - bodyclose 57 | - errorlint 58 | - exhaustive 59 | - goconst 60 | - gofmt 61 | - goimports 62 | - govet 63 | - nilerr 64 | - ineffassign 65 | - wrapcheck 66 | - unconvert 67 | - unparam 68 | - unused 69 | - staticcheck 70 | - wastedassign 71 | - whitespace 72 | disable: 73 | - gochecknoglobals 74 | - golint 75 | - gocritic 76 | - wsl 77 | issues: 78 | # Excluding configuration per-path, per-linter, per-text and per-source 79 | exclude: 80 | - only cuddled expressions if assigning variable or using from line above 81 | - ranges should only be cuddled with assignments used in the iteration 82 | - defer statements should only be cuddled with expressions on same variable 83 | - append only allowed to cuddle with appended value 84 | - if statements should only be cuddled with assignments used in the if statement itself 85 | exclude-rules: 86 | # Exclude some linters from running on tests files. 87 | - path: _test\.go 88 | linters: 89 | - gocyclo 90 | - asciicheck 91 | - bodyclose 92 | - errorlint 93 | - exhaustive 94 | - goconst 95 | - gocritic 96 | - gofmt 97 | - goimports 98 | - govet 99 | - nilerr 100 | - ineffassign 101 | - wrapcheck 102 | - unconvert 103 | - unparam 104 | - unused 105 | - staticcheck 106 | - wastedassign 107 | - wsl 108 | - whitespace 109 | - path: _mock\.go 110 | linters: 111 | - gocyclo 112 | - asciicheck 113 | - bodyclose 114 | - errorlint 115 | - exhaustive 116 | - goconst 117 | - gocritic 118 | - gofmt 119 | - goimports 120 | - govet 121 | - nilerr 122 | - ineffassign 123 | - wrapcheck 124 | - unconvert 125 | - unparam 126 | - unused 127 | - staticcheck 128 | - wastedassign 129 | - wsl 130 | - whitespace 131 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PORT?=9999 2 | APP_NAME?=test-app 3 | MOQPATH := $(shell pwd)/bin/ 4 | 5 | clean: 6 | rm -f ${APP_NAME} 7 | 8 | build: clean 9 | go build -o ${APP_NAME} 10 | 11 | run: build 12 | PORT=${PORT} ./${APP_NAME} 13 | 14 | test: 15 | go test -v -count=1 ./... 16 | 17 | test100: 18 | go test -v -count=100 ./... 19 | 20 | race: 21 | go test -v -race -count=1 ./... 22 | 23 | .PHONY: cover 24 | cover: 25 | go test -short -count=1 -race -coverprofile=coverage.out ./... 26 | go tool cover -html=coverage.out 27 | rm coverage.out 28 | 29 | .PHONY: gen 30 | gen: 31 | go generate internal/... 32 | mockgen -source=internal/pkg/repository/order/repository.go \ 33 | -destination=internal/pkg/repository/order/mocks/mock_repository.go 34 | 35 | PHONE: lint 36 | lint: 37 | @(golangci-lint run) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### What it is 2 | Example of transactional outbox pattern in golang: write new orders with api server, send them to kafka via cron. 3 | 4 | More abort pattern: https://microservices.io/patterns/data/transactional-outbox.html 5 | 6 | ### How to run 7 | ``` 8 | // to run postgres, kafka 9 | docker-compose up -d 10 | 11 | // to run app 12 | go run cmd/main.go --conf=conf.yaml 13 | 14 | // to run worker once 15 | go run cmd/cron/outbox_producer/order_producer/main.go --conf=conf.yaml 16 | ``` 17 | -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | type Config struct { 12 | AppPort string `yaml:"port"` 13 | DbConnString string `yaml:"db_conn_string"` 14 | KafkaPort string `yaml:"kafka_port"` 15 | } 16 | 17 | func Parse(confPath string) (*Config, error) { 18 | filename, err := filepath.Abs(confPath) 19 | if err != nil { 20 | return nil, fmt.Errorf("can't get config path: %s", err.Error()) 21 | } 22 | 23 | yamlConf, err := os.ReadFile(filename) 24 | if err != nil { 25 | return nil, fmt.Errorf("can't read conf: %s", err.Error()) 26 | } 27 | 28 | var config Config 29 | 30 | err = yaml.Unmarshal(yamlConf, &config) 31 | if err != nil { 32 | return nil, fmt.Errorf("can't unmarshall conf: %s", err.Error()) 33 | } 34 | 35 | return &config, nil 36 | } 37 | -------------------------------------------------------------------------------- /cmd/cron/outbox_procuder/order_producer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/ansakharov/lets_test/cmd/config" 9 | "github.com/ansakharov/lets_test/internal/cron/outbox_producer" 10 | "github.com/ansakharov/lets_test/internal/kafka" 11 | "github.com/ansakharov/lets_test/internal/pkg/repository/order" 12 | "github.com/jackc/pgx/v4/pgxpool" 13 | "github.com/pkg/errors" 14 | 15 | "github.com/ansakharov/lets_test/logger" 16 | _ "github.com/jackc/pgx/v4/pgxpool" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | func main() { 21 | // Get logger interface. 22 | l := logger.New() 23 | 24 | if err := mainNoExit(l); err != nil { 25 | l.Fatalf("fatal err: %s", err.Error()) 26 | } 27 | } 28 | 29 | func mainNoExit(log logrus.FieldLogger) error { 30 | // get application cfg 31 | confFlag := flag.String("conf", "", "cfg yaml file") 32 | flag.Parse() 33 | 34 | confString := *confFlag 35 | if confString == "" { 36 | return fmt.Errorf(" 'conf' flag required") 37 | } 38 | 39 | cfg, err := config.Parse(confString) 40 | if err != nil { 41 | return errors.Wrap(err, "config.Parse") 42 | } 43 | 44 | log.Println(cfg) 45 | log.Println("Starting the service...") 46 | 47 | ctx := context.Background() 48 | 49 | producer := kafka.NewProducer(cfg.KafkaPort) 50 | 51 | pool, err := pgxpool.Connect(context.Background(), cfg.DbConnString) 52 | if err != nil { 53 | return fmt.Errorf("can't create pg pool: %s", err.Error()) 54 | } 55 | 56 | outboxProducer := outbox_producer.New(producer, pool, order.OutboxTable, log) 57 | 58 | return errors.Wrap(outboxProducer.ProduceMessages(ctx), "outboxProducer.ProduceMessages") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/ansakharov/lets_test/cmd/config" 10 | create_order_handler "github.com/ansakharov/lets_test/handler/create_order" 11 | echo_handler "github.com/ansakharov/lets_test/handler/echo" 12 | get_orders_handler "github.com/ansakharov/lets_test/handler/get_orders" 13 | "github.com/ansakharov/lets_test/internal/kafka" 14 | orderRepo "github.com/ansakharov/lets_test/internal/pkg/repository/order" 15 | orderUCase "github.com/ansakharov/lets_test/internal/usecase/order" 16 | "github.com/gorilla/mux" 17 | "github.com/jackc/pgx/v4/pgxpool" 18 | "github.com/pkg/errors" 19 | 20 | "github.com/ansakharov/lets_test/logger" 21 | _ "github.com/jackc/pgx/v4/pgxpool" 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | const ( 26 | echoRoute = "/echo" 27 | orderRoute = "/order" 28 | ordersRoute = "/orders" 29 | ) 30 | 31 | func main() { 32 | // Get logger interface. 33 | l := logger.New() 34 | 35 | if err := mainNoExit(l); err != nil { 36 | l.Fatalf("fatal err: %s", err.Error()) 37 | } 38 | } 39 | 40 | func mainNoExit(log logrus.FieldLogger) error { 41 | // get application cfg 42 | confFlag := flag.String("conf", "", "cfg yaml file") 43 | flag.Parse() 44 | 45 | confString := *confFlag 46 | if confString == "" { 47 | return fmt.Errorf(" 'conf' flag required") 48 | } 49 | 50 | cfg, err := config.Parse(confString) 51 | if err != nil { 52 | return errors.Wrap(err, "config.Parse") 53 | } 54 | 55 | log.Println(cfg) 56 | log.Println("Starting the service...") 57 | 58 | ctx := context.Background() 59 | 60 | r := mux.NewRouter() 61 | 62 | // echo 63 | r.HandleFunc(echoRoute, echo_handler.Handler("Your message: ").ServeHTTP).Methods("GET") 64 | 65 | pool, err := pgxpool.Connect(context.Background(), cfg.DbConnString) 66 | if err != nil { 67 | return fmt.Errorf("can't create pg pool: %s", err.Error()) 68 | } 69 | repo := orderRepo.New(pool) 70 | orderUseCase := orderUCase.New(repo, kafka.NewProducer(cfg.KafkaPort)) 71 | 72 | createOrderHandleFunc := create_order_handler.New(orderUseCase, log).Create(ctx).ServeHTTP 73 | // create order 74 | r.HandleFunc(orderRoute, createOrderHandleFunc).Methods("POST") 75 | 76 | getOrderHandlerFunc := get_orders_handler.New(orderUseCase, log).Get(ctx).ServeHTTP 77 | // get orders 78 | r.HandleFunc(ordersRoute, getOrderHandlerFunc).Methods("GET") 79 | 80 | if err != nil { 81 | return fmt.Errorf("can't init router: %s", err.Error()) 82 | } 83 | 84 | log.Print("The service is ready to listen and serve.") 85 | 86 | return errors.Wrap(http.ListenAndServe( 87 | cfg.AppPort, 88 | r, 89 | ), "http.ListenAndServe") 90 | } 91 | -------------------------------------------------------------------------------- /conf.yaml: -------------------------------------------------------------------------------- 1 | port: ":40999" 2 | db_conn_string: "postgres://postgres:changeme@localhost:5432/postgres" 3 | kafka_port: "9092" -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | zookeeper: 4 | image: 'bitnami/zookeeper:latest' 5 | ports: 6 | - '2181:2181' 7 | environment: 8 | - ALLOW_ANONYMOUS_LOGIN=yes 9 | kafka: 10 | image: 'bitnami/kafka:2.7.0' 11 | ports: 12 | - '9092:9092' 13 | depends_on: 14 | - zookeeper 15 | environment: 16 | - ALLOW_PLAINTEXT_LISTENER=yes 17 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 18 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092 19 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 20 | pg: 21 | image: postgres:10 22 | environment: 23 | POSTGRES_USER: ${POSTGRES_USER:-postgres} 24 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} 25 | POSTGRES_HOST_AUTH_METHOD: trust 26 | ports: 27 | - "5432:5432" 28 | volumes: 29 | - pgdata:/var/lib/postgresql/data 30 | 31 | volumes: 32 | pgdata: 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ansakharov/lets_test 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/Masterminds/squirrel v1.5.2 7 | github.com/Shopify/sarama v1.38.1 8 | github.com/golang/mock v1.6.0 9 | github.com/gorilla/mux v1.8.0 10 | github.com/hashicorp/go-uuid v1.0.3 11 | github.com/jackc/pgx/v4 v4.15.0 12 | github.com/lib/pq v1.10.2 13 | github.com/pkg/errors v0.9.1 14 | github.com/sirupsen/logrus v1.8.1 15 | github.com/stretchr/testify v1.8.1 16 | golang.org/x/sync v0.1.0 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/eapache/go-resiliency v1.3.0 // indirect 23 | github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect 24 | github.com/eapache/queue v1.1.0 // indirect 25 | github.com/golang/snappy v0.0.4 // indirect 26 | github.com/hashicorp/errwrap v1.0.0 // indirect 27 | github.com/hashicorp/go-multierror v1.1.1 // indirect 28 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 29 | github.com/jackc/pgconn v1.11.0 // indirect 30 | github.com/jackc/pgio v1.0.0 // indirect 31 | github.com/jackc/pgpassfile v1.0.0 // indirect 32 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect 33 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 34 | github.com/jackc/pgtype v1.10.0 // indirect 35 | github.com/jackc/puddle v1.2.1 // indirect 36 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 37 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 38 | github.com/jcmturner/gofork v1.7.6 // indirect 39 | github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect 40 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 41 | github.com/klauspost/compress v1.15.14 // indirect 42 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 43 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 44 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 47 | github.com/rogpeppe/go-internal v1.11.0 // indirect 48 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 49 | golang.org/x/net v0.5.0 // indirect 50 | golang.org/x/sys v0.4.0 // indirect 51 | golang.org/x/text v0.6.0 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 3 | github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE= 4 | github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 5 | github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= 6 | github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= 7 | github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= 8 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 9 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 10 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 11 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 12 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= 17 | github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= 18 | github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= 19 | github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= 20 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 21 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 22 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 23 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 24 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 25 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 26 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 27 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 28 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 29 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 30 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 31 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 32 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 33 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 34 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 35 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 36 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 37 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 38 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 39 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 40 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 41 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 42 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 43 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 44 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 45 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 46 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 47 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 48 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 49 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 50 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 51 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 52 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 53 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 54 | github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= 55 | github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 56 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 57 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 58 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 59 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 60 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 61 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 62 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 63 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 64 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 65 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 66 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 67 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 68 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 69 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 70 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 71 | github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= 72 | github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 73 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 74 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 75 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 76 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 77 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 78 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 79 | github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= 80 | github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 81 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 82 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 83 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 84 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 85 | github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= 86 | github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= 87 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 88 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 89 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 90 | github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= 91 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 92 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 93 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 94 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 95 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 96 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 97 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 98 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 99 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 100 | github.com/jcmturner/gokrb5/v8 v8.4.3 h1:iTonLeSJOn7MVUtyMT+arAn5AKAPrkilzhGw8wE/Tq8= 101 | github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0= 102 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 103 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 104 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 105 | github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc= 106 | github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= 107 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 108 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 109 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 110 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 111 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 112 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 113 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 114 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 115 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 116 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 117 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 118 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 119 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 120 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 121 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 122 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 123 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 124 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 125 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 126 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 127 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 128 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 129 | github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= 130 | github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 131 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 132 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 133 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 134 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 135 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 136 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 137 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 138 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 139 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 140 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 141 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 142 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 143 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 144 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 145 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 146 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 147 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 148 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 149 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 150 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 151 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 152 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 153 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 154 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 155 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 156 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 157 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 158 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 159 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 160 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 161 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 162 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 163 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 164 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 165 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 166 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 167 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 168 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 169 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 170 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 171 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 172 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 173 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 174 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 175 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 176 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 177 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 178 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 179 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 180 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 181 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 182 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 183 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 184 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 185 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 186 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 187 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 188 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= 189 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 190 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 191 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 192 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 193 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 194 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 195 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 196 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 197 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 198 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 199 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 200 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 201 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 202 | golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= 203 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 204 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 205 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 207 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 208 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 209 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 210 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 211 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 223 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 224 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 225 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 226 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 227 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 228 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 229 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 230 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 231 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 232 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 233 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 234 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 235 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 236 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 237 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 238 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 239 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 240 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 241 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 242 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 243 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 244 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 245 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 246 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 247 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 248 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 250 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 251 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 252 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 253 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 254 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 255 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 256 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 257 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 258 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 259 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 260 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 261 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 262 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 263 | -------------------------------------------------------------------------------- /handler/create_order/create.go: -------------------------------------------------------------------------------- 1 | package order_handler 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | 9 | "github.com/ansakharov/lets_test/internal/pkg/entity/order" 10 | ) 11 | 12 | var ( 13 | ErrInvalidUserID = errors.New("invalid user OrderID") 14 | ErrInvalidAmount = errors.New("invalid price") 15 | ErrInvalidPaymentType = errors.New("invalid payment type") 16 | ErrEmptyItems = errors.New("items can't be empty") 17 | ErrInvalidItemID = errors.New("invalid service id") 18 | ) 19 | 20 | // OrderFromDTO creates Order for business layer. 21 | func (in OrderIn) OrderFromDTO() order.Order { 22 | items := make([]order.Item, 0, len(in.Items)) 23 | for _, item := range in.Items { 24 | items = append(items, order.Item{ 25 | ID: item.ID, 26 | Amount: item.Amount, 27 | DiscountedAmount: item.Discount, 28 | }) 29 | } 30 | 31 | return order.Order{ 32 | Status: order.CreatedStatus, 33 | UserID: in.UserID, 34 | PaymentType: order.PaymentType(paymentTypes[in.PaymentType]), 35 | Items: items, 36 | } 37 | } 38 | 39 | // Create responsible for saving new order. 40 | func (h Handler) Create(ctx context.Context) http.Handler { 41 | fn := func(w http.ResponseWriter, r *http.Request) { 42 | // prepare dto to parse request 43 | in := &OrderIn{} 44 | // parse req body to dto 45 | err := json.NewDecoder(r.Body).Decode(&in) 46 | if err != nil { 47 | h.log.Errorf("can't parse req: %s", err.Error()) 48 | http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest) 49 | return 50 | } 51 | 52 | // check that request valid 53 | err = h.validateReq(in) 54 | if err != nil { 55 | h.log.Errorf("bad req: %v: %s", in, err.Error()) 56 | http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest) 57 | return 58 | } 59 | 60 | saveOrder := in.OrderFromDTO() 61 | err = h.uCase.Save(ctx, h.log, &saveOrder) 62 | if err != nil { 63 | h.log.Errorf("uCase.Save: %v", err) 64 | http.Error(w, "can't create saveOrder: "+err.Error(), http.StatusInternalServerError) 65 | return 66 | } 67 | 68 | w.Header().Set("Content-Type", "application/json") 69 | 70 | m := make(map[string]interface{}) 71 | m["success"] = "ok" 72 | 73 | err = json.NewEncoder(w).Encode(m) 74 | if err != nil { 75 | h.log.Errorf("Encode: %v", err) 76 | } 77 | } 78 | return http.HandlerFunc(fn) 79 | } 80 | -------------------------------------------------------------------------------- /handler/create_order/create_test.go: -------------------------------------------------------------------------------- 1 | package order_handler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestValidate(t *testing.T) { 10 | h := Handler{} 11 | in := &OrderIn{ 12 | UserID: 1, 13 | PaymentType: "card", 14 | Items: []Item{ 15 | {ID: 1, Amount: 10}, 16 | }, 17 | } 18 | err := h.validateReq(in) 19 | require.NoError(t, err) 20 | } 21 | 22 | func TestValidateError(t *testing.T) { 23 | cases := []struct { 24 | name string 25 | in *OrderIn 26 | expErr error 27 | }{ 28 | { 29 | name: "bad_user_id", 30 | in: &OrderIn{UserID: 0}, 31 | expErr: ErrInvalidUserID, 32 | }, 33 | { 34 | name: "bad_payment_type", 35 | in: &OrderIn{UserID: 1, PaymentType: "bad"}, 36 | expErr: ErrInvalidPaymentType, 37 | }, 38 | { 39 | name: "no_items", 40 | in: &OrderIn{UserID: 1, PaymentType: "card"}, 41 | expErr: ErrEmptyItems, 42 | }, 43 | { 44 | name: "bad_item_id", 45 | in: &OrderIn{ 46 | UserID: 1, 47 | PaymentType: "card", 48 | Items: []Item{ 49 | {ID: 0}, 50 | }, 51 | }, 52 | expErr: ErrInvalidItemID, 53 | }, 54 | { 55 | name: "bad_item_amount", 56 | in: &OrderIn{ 57 | UserID: 1, 58 | PaymentType: "card", 59 | Items: []Item{ 60 | {ID: 1, Amount: 0}, 61 | }, 62 | }, 63 | expErr: ErrInvalidAmount, 64 | }, 65 | } 66 | h := Handler{} 67 | for _, tCase := range cases { 68 | t.Run(tCase.name, func(t *testing.T) { 69 | err := h.validateReq(tCase.in) 70 | require.Error(t, err) 71 | require.EqualError(t, tCase.expErr, err.Error()) 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /handler/create_order/domain.go: -------------------------------------------------------------------------------- 1 | package order_handler 2 | 3 | import ( 4 | create_order "github.com/ansakharov/lets_test/internal/usecase/order" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // Handler creates orders 9 | type Handler struct { 10 | uCase *create_order.Usecase 11 | log logrus.FieldLogger 12 | } 13 | 14 | // New gives Handler. 15 | func New( 16 | uCase *create_order.Usecase, 17 | log logrus.FieldLogger, 18 | ) *Handler { 19 | return &Handler{ 20 | uCase: uCase, 21 | log: log, 22 | } 23 | } 24 | 25 | // OrderIn is dto for http req. 26 | type OrderIn struct { 27 | UserID uint64 `json:"user_id"` 28 | PaymentType string `json:"payment_type"` 29 | Items []Item `json:"items"` 30 | } 31 | 32 | type Item struct { 33 | ID uint64 `json:"id"` 34 | Amount uint64 `json:"amount"` 35 | Discount uint64 `json:"discount"` 36 | } 37 | 38 | var paymentTypes = map[string]PaymentType{ 39 | "card": Card, 40 | "wallet": Wallet, 41 | } 42 | 43 | type PaymentType uint8 44 | 45 | const ( 46 | UndefinedType PaymentType = iota 47 | Card 48 | Wallet 49 | ) 50 | -------------------------------------------------------------------------------- /handler/create_order/validate.go: -------------------------------------------------------------------------------- 1 | package order_handler 2 | 3 | // validates request. 4 | func (h Handler) validateReq(in *OrderIn) error { 5 | // user OrderID can't be 0 6 | if in.UserID == 0 { 7 | return ErrInvalidUserID 8 | } 9 | // payment type must be in paymentTypes 10 | if _, ok := paymentTypes[in.PaymentType]; !ok { 11 | return ErrInvalidPaymentType 12 | } 13 | // no services passed in request 14 | if len(in.Items) == 0 { 15 | return ErrEmptyItems 16 | } 17 | // service doesn't contain valid id 18 | for i := range in.Items { 19 | if in.Items[i].ID == 0 { 20 | return ErrInvalidItemID 21 | } 22 | 23 | if in.Items[i].Amount == 0 { 24 | return ErrInvalidAmount 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /handler/echo/handler.go: -------------------------------------------------------------------------------- 1 | package echo_handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Handler prints request. 9 | func Handler(greet string) http.Handler { 10 | fn := func(w http.ResponseWriter, r *http.Request) { 11 | _, err := w.Write([]byte(greet + r.URL.Query().Encode())) 12 | if err != nil { 13 | fmt.Printf("err: %v", err) 14 | } 15 | } 16 | return http.HandlerFunc(fn) 17 | } 18 | -------------------------------------------------------------------------------- /handler/get_orders/get.go: -------------------------------------------------------------------------------- 1 | package order_handler 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | 9 | order_ucase "github.com/ansakharov/lets_test/internal/usecase/order" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Requst validation errors. 14 | var ErrEmptyOrderIDs = errors.New("no order ids passed") 15 | 16 | // Handler creates orders 17 | type Handler struct { 18 | uCase *order_ucase.Usecase 19 | log logrus.FieldLogger 20 | } 21 | 22 | // New gives Handler. 23 | func New( 24 | uCase *order_ucase.Usecase, 25 | log logrus.FieldLogger, 26 | ) *Handler { 27 | return &Handler{ 28 | uCase: uCase, 29 | log: log, 30 | } 31 | } 32 | 33 | // GetOrdersIn is dto for http req. 34 | type GetOrdersIn struct { 35 | IDs []uint64 `json:"ids"` 36 | } 37 | 38 | // validates request. 39 | func (h Handler) validateReq(in *GetOrdersIn) error { 40 | if len(in.IDs) == 0 { 41 | return ErrEmptyOrderIDs 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // Create responsible for saving new order. 48 | func (h Handler) Get(ctx context.Context) http.Handler { 49 | fn := func(w http.ResponseWriter, r *http.Request) { 50 | // prepare dto to parse request 51 | in := &GetOrdersIn{} 52 | // parse req body to dto 53 | err := json.NewDecoder(r.Body).Decode(&in) 54 | if err != nil { 55 | h.log.Errorf("can't parse req: %s", err.Error()) 56 | http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest) 57 | 58 | return 59 | } 60 | 61 | // check that request valid 62 | err = h.validateReq(in) 63 | if err != nil { 64 | h.log.Errorf("bad req: %v: %s", in, err.Error()) 65 | http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest) 66 | 67 | return 68 | } 69 | 70 | orders, err := h.uCase.Get(ctx, h.log, in.IDs) 71 | if err != nil { 72 | h.log.Errorf("can't get orders: %s", err.Error()) 73 | http.Error(w, "can't get orders: "+err.Error(), http.StatusInternalServerError) 74 | 75 | return 76 | } 77 | 78 | w.Header().Set("Content-Type", "application/json") 79 | 80 | if err := json.NewEncoder(w).Encode(orders); err != nil { 81 | h.log.Errorf("Encode: %v", err) 82 | } 83 | } 84 | 85 | return http.HandlerFunc(fn) 86 | } 87 | -------------------------------------------------------------------------------- /handler/get_orders/get_test.go: -------------------------------------------------------------------------------- 1 | package order_handler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestValidate(t *testing.T) { 10 | h := Handler{} 11 | in := &GetOrdersIn{ 12 | IDs: []uint64{3, 2, 1}, 13 | } 14 | err := h.validateReq(in) 15 | require.NoError(t, err) 16 | } 17 | 18 | func TestValidateError(t *testing.T) { 19 | h := Handler{} 20 | in := &GetOrdersIn{ 21 | IDs: nil, 22 | } 23 | err := h.validateReq(in) 24 | require.Error(t, err) 25 | } 26 | -------------------------------------------------------------------------------- /internal/cron/outbox_producer/outbox_producer.go: -------------------------------------------------------------------------------- 1 | package outbox_producer 2 | 3 | import ( 4 | "context" 5 | baseErr "errors" 6 | "fmt" 7 | 8 | sq "github.com/Masterminds/squirrel" 9 | "github.com/Shopify/sarama" 10 | "github.com/ansakharov/lets_test/internal/kafka" 11 | "github.com/ansakharov/lets_test/internal/pkg/repository/order" 12 | "github.com/jackc/pgx/v4" 13 | "github.com/jackc/pgx/v4/pgxpool" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type OutboxProducer struct { 18 | producer sarama.SyncProducer 19 | // TODO replace with interface 20 | db *pgxpool.Pool 21 | outboxTable string 22 | log logrus.FieldLogger 23 | } 24 | 25 | type outboxMessage struct { 26 | OrderID int64 `db:"order_id"` 27 | EventID string `db:"event_id"` 28 | } 29 | 30 | func New(producer sarama.SyncProducer, db *pgxpool.Pool, outboxTable string, log logrus.FieldLogger) *OutboxProducer { 31 | return &OutboxProducer{ 32 | producer: producer, 33 | db: db, 34 | outboxTable: outboxTable, 35 | log: log, 36 | } 37 | } 38 | 39 | // ProduceMessages from outbox table 40 | func (op *OutboxProducer) ProduceMessages(ctx context.Context) (err error) { 41 | tx, err := op.db.BeginTx(ctx, pgx.TxOptions{}) 42 | // вычитаем данные на отправку 43 | defer func() { 44 | if err != nil { 45 | rollbackErr := tx.Rollback(ctx) 46 | if rollbackErr != nil { 47 | err = baseErr.Join(err, rollbackErr) 48 | } 49 | } 50 | }() 51 | // build query. 52 | query, args, err := sq. 53 | Select("event_id", "order_id"). 54 | From(order.OutboxTable). 55 | Where(sq.Eq{"sent": false}). 56 | OrderBy("order_id asc"). 57 | Limit(100). 58 | PlaceholderFormat(sq.Dollar). 59 | ToSql() 60 | if err != nil { 61 | return fmt.Errorf("can't build query: %s", err.Error()) 62 | } 63 | 64 | rows, err := tx.Query(ctx, query, args...) 65 | if err != nil { 66 | return err 67 | } 68 | defer rows.Close() 69 | 70 | //messages := make([]outboxMessage, 0, 100) 71 | eventIds := []string{} 72 | saramaMsgs := make([]*sarama.ProducerMessage, 0, 100) 73 | 74 | for rows.Next() { 75 | msg := outboxMessage{} 76 | if err := rows.Scan(&msg.EventID, &msg.OrderID); err != nil { 77 | return err 78 | } 79 | 80 | // messages = append(messages, msg) 81 | saramaMsgs = append(saramaMsgs, &sarama.ProducerMessage{ 82 | Topic: kafka.Topic, 83 | Value: sarama.StringEncoder(fmt.Sprintf("{\"event_id\": \"%s\", \"order_id\": %d}", msg.EventID, msg.OrderID)), 84 | }) 85 | 86 | eventIds = append(eventIds, msg.EventID) 87 | } 88 | 89 | // отправим данные 90 | 91 | err = op.producer.SendMessages(saramaMsgs) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | query, args, err = sq. 97 | Update(op.outboxTable). 98 | Set("sent", true). 99 | Where(sq.Eq{"event_id": eventIds}). 100 | PlaceholderFormat(sq.Dollar).ToSql() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | // пометим данным отправленными 106 | 107 | if _, err := tx.Exec(ctx, query, args...); err != nil { 108 | return fmt.Errorf("tx.Exec: %s", err.Error()) 109 | } 110 | 111 | // коммит 112 | return tx.Commit(ctx) 113 | } 114 | -------------------------------------------------------------------------------- /internal/kafka/domain.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/Shopify/sarama" 8 | ) 9 | 10 | const Topic = "quickstart-events" 11 | 12 | func NewProducer(port string) sarama.SyncProducer { 13 | // Конфигурация Kafka Producer 14 | cfg := sarama.NewConfig() 15 | cfg.Producer.RequiredAcks = sarama.WaitForAll 16 | cfg.Producer.Return.Successes = true 17 | 18 | // Создаем Kafka Producer 19 | producer, err := sarama.NewSyncProducer([]string{fmt.Sprintf("localhost:%s", port)}, cfg) 20 | if err != nil { 21 | log.Fatalln("Failed to start Sarama producer:", err) 22 | } 23 | 24 | return producer 25 | } 26 | -------------------------------------------------------------------------------- /internal/pkg/entity/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | // Order represents clients order. 4 | type Order struct { 5 | ID uint64 6 | Status Status 7 | UserID uint64 8 | PaymentType PaymentType 9 | OriginalAmount uint64 10 | DiscountedAmount uint64 11 | Items []Item 12 | } 13 | 14 | // Order status. 15 | type Status uint8 16 | 17 | const ( 18 | UnknownStatus Status = iota 19 | CreatedStatus 20 | ProcessedStatus 21 | CanceledStatus 22 | ) 23 | 24 | // Way of payment 25 | type PaymentType uint8 26 | 27 | const ( 28 | UnknownType PaymentType = iota 29 | Card 30 | Wallet 31 | ) 32 | 33 | type Item struct { 34 | OrderID uint64 `db:"order_id"` 35 | ID uint64 `db:"item_id"` 36 | Amount uint64 `db:"amount"` 37 | DiscountedAmount uint64 `db:"discounted_amount"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/pkg/repository/order/fake_order_repo/repository.go: -------------------------------------------------------------------------------- 1 | package fake_order 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ansakharov/lets_test/internal/pkg/entity/order" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type Repository struct { 11 | orders map[uint64]*order.Order 12 | currID uint64 13 | } 14 | 15 | // New instance of repository. 16 | func New() *Repository { 17 | return &Repository{ 18 | orders: make(map[uint64]*order.Order), 19 | currID: 1, 20 | } 21 | } 22 | 23 | // Save new order to DB. 24 | func (r *Repository) Save(ctx context.Context, log logrus.FieldLogger, order *order.Order) error { 25 | order.ID = r.currID 26 | for idx, item := range order.Items { 27 | item.OrderID = r.currID 28 | order.Items[idx] = item 29 | } 30 | r.orders[r.currID] = order 31 | r.currID++ 32 | 33 | return nil 34 | } 35 | 36 | // Get returns map of orders. 37 | func (r *Repository) Get(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order.Order, error) { 38 | result := make(map[uint64]order.Order) 39 | 40 | for _, ID := range IDs { 41 | localOrder, ok := r.orders[ID] 42 | if ok { 43 | result[ID] = *localOrder 44 | } 45 | } 46 | 47 | return result, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/pkg/repository/order/mocks/mock_repository.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/pkg/repository/order/repository.go 3 | 4 | // Package mock_order is a generated GoMock package. 5 | package mock_order 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | order "github.com/ansakharov/lets_test/internal/pkg/entity/order" 12 | gomock "github.com/golang/mock/gomock" 13 | logrus "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // MockOrderRepo is a mock of OrderRepo interface. 17 | type MockOrderRepo struct { 18 | ctrl *gomock.Controller 19 | recorder *MockOrderRepoMockRecorder 20 | } 21 | 22 | // MockOrderRepoMockRecorder is the mock recorder for MockOrderRepo. 23 | type MockOrderRepoMockRecorder struct { 24 | mock *MockOrderRepo 25 | } 26 | 27 | // NewMockOrderRepo creates a new mock instance. 28 | func NewMockOrderRepo(ctrl *gomock.Controller) *MockOrderRepo { 29 | mock := &MockOrderRepo{ctrl: ctrl} 30 | mock.recorder = &MockOrderRepoMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockOrderRepo) EXPECT() *MockOrderRepoMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Get mocks base method. 40 | func (m *MockOrderRepo) Get(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order.Order, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Get", ctx, log, IDs) 43 | ret0, _ := ret[0].(map[uint64]order.Order) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Get indicates an expected call of Get. 49 | func (mr *MockOrderRepoMockRecorder) Get(ctx, log, IDs interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockOrderRepo)(nil).Get), ctx, log, IDs) 52 | } 53 | 54 | // Save mocks base method. 55 | func (m *MockOrderRepo) Save(ctx context.Context, log logrus.FieldLogger, order *order.Order) error { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "Save", ctx, log, order) 58 | ret0, _ := ret[0].(error) 59 | return ret0 60 | } 61 | 62 | // Save indicates an expected call of Save. 63 | func (mr *MockOrderRepoMockRecorder) Save(ctx, log, order interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockOrderRepo)(nil).Save), ctx, log, order) 66 | } 67 | -------------------------------------------------------------------------------- /internal/pkg/repository/order/mocks2/repo_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by moq; DO NOT EDIT. 2 | // github.com/matryer/moq 3 | 4 | package mocks 5 | 6 | import ( 7 | "context" 8 | order_entity "github.com/ansakharov/lets_test/internal/pkg/entity/order" 9 | "github.com/sirupsen/logrus" 10 | "sync" 11 | ) 12 | 13 | // OrderRepoMock is a mock implementation of order.OrderRepo. 14 | // 15 | // func TestSomethingThatUsesOrderRepo(t *testing.T) { 16 | // 17 | // // make and configure a mocked order.OrderRepo 18 | // mockedOrderRepo := &OrderRepoMock{ 19 | // GetFunc: func(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order_entity.Order, error) { 20 | // panic("mock out the Get method") 21 | // }, 22 | // SaveFunc: func(ctx context.Context, log logrus.FieldLogger, order *order_entity.Order) (uint, error) { 23 | // panic("mock out the Save method") 24 | // }, 25 | // } 26 | // 27 | // // use mockedOrderRepo in code that requires order.OrderRepo 28 | // // and then make assertions. 29 | // 30 | // } 31 | type OrderRepoMock struct { 32 | // GetFunc mocks the Get method. 33 | GetFunc func(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order_entity.Order, error) 34 | 35 | // SaveFunc mocks the Save method. 36 | SaveFunc func(ctx context.Context, log logrus.FieldLogger, order *order_entity.Order) (uint, error) 37 | 38 | // calls tracks calls to the methods. 39 | calls struct { 40 | // Get holds details about calls to the Get method. 41 | Get []struct { 42 | // Ctx is the ctx argument value. 43 | Ctx context.Context 44 | // Log is the log argument value. 45 | Log logrus.FieldLogger 46 | // IDs is the IDs argument value. 47 | IDs []uint64 48 | } 49 | // Save holds details about calls to the Save method. 50 | Save []struct { 51 | // Ctx is the ctx argument value. 52 | Ctx context.Context 53 | // Log is the log argument value. 54 | Log logrus.FieldLogger 55 | // Order is the order argument value. 56 | Order *order_entity.Order 57 | } 58 | } 59 | lockGet sync.RWMutex 60 | lockSave sync.RWMutex 61 | } 62 | 63 | // Get calls GetFunc. 64 | func (mock *OrderRepoMock) Get(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order_entity.Order, error) { 65 | if mock.GetFunc == nil { 66 | panic("OrderRepoMock.GetFunc: method is nil but OrderRepo.Get was just called") 67 | } 68 | callInfo := struct { 69 | Ctx context.Context 70 | Log logrus.FieldLogger 71 | IDs []uint64 72 | }{ 73 | Ctx: ctx, 74 | Log: log, 75 | IDs: IDs, 76 | } 77 | mock.lockGet.Lock() 78 | mock.calls.Get = append(mock.calls.Get, callInfo) 79 | mock.lockGet.Unlock() 80 | return mock.GetFunc(ctx, log, IDs) 81 | } 82 | 83 | // GetCalls gets all the calls that were made to Get. 84 | // Check the length with: 85 | // 86 | // len(mockedOrderRepo.GetCalls()) 87 | func (mock *OrderRepoMock) GetCalls() []struct { 88 | Ctx context.Context 89 | Log logrus.FieldLogger 90 | IDs []uint64 91 | } { 92 | var calls []struct { 93 | Ctx context.Context 94 | Log logrus.FieldLogger 95 | IDs []uint64 96 | } 97 | mock.lockGet.RLock() 98 | calls = mock.calls.Get 99 | mock.lockGet.RUnlock() 100 | return calls 101 | } 102 | 103 | // Save calls SaveFunc. 104 | func (mock *OrderRepoMock) Save(ctx context.Context, log logrus.FieldLogger, order *order_entity.Order) (uint, error) { 105 | if mock.SaveFunc == nil { 106 | panic("OrderRepoMock.SaveFunc: method is nil but OrderRepo.Save was just called") 107 | } 108 | callInfo := struct { 109 | Ctx context.Context 110 | Log logrus.FieldLogger 111 | Order *order_entity.Order 112 | }{ 113 | Ctx: ctx, 114 | Log: log, 115 | Order: order, 116 | } 117 | mock.lockSave.Lock() 118 | mock.calls.Save = append(mock.calls.Save, callInfo) 119 | mock.lockSave.Unlock() 120 | return mock.SaveFunc(ctx, log, order) 121 | } 122 | 123 | // SaveCalls gets all the calls that were made to Save. 124 | // Check the length with: 125 | // 126 | // len(mockedOrderRepo.SaveCalls()) 127 | func (mock *OrderRepoMock) SaveCalls() []struct { 128 | Ctx context.Context 129 | Log logrus.FieldLogger 130 | Order *order_entity.Order 131 | } { 132 | var calls []struct { 133 | Ctx context.Context 134 | Log logrus.FieldLogger 135 | Order *order_entity.Order 136 | } 137 | mock.lockSave.RLock() 138 | calls = mock.calls.Save 139 | mock.lockSave.RUnlock() 140 | return calls 141 | } 142 | -------------------------------------------------------------------------------- /internal/pkg/repository/order/repository.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "context" 5 | baseErr "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/Masterminds/squirrel" 10 | sq "github.com/Masterminds/squirrel" 11 | "github.com/ansakharov/lets_test/internal/pkg/entity/order" 12 | order_entity "github.com/ansakharov/lets_test/internal/pkg/entity/order" 13 | "github.com/hashicorp/go-uuid" 14 | "github.com/jackc/pgx/v4" 15 | "github.com/jackc/pgx/v4/pgxpool" 16 | "github.com/pkg/errors" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | const ( 21 | // tables 22 | ordersTable = "orders" 23 | itemsTable = "items" 24 | orderItemsTable = "order_items" 25 | 26 | OutboxTable = "outbox" 27 | ) 28 | 29 | type Repository struct { 30 | db *pgxpool.Pool 31 | } 32 | 33 | //go:generate ${MOQPATH}moq -skip-ensure -pkg mocks -out ./mocks2/repo_mock.go . OrderRepo 34 | type OrderRepo interface { 35 | Save(ctx context.Context, log logrus.FieldLogger, order *order_entity.Order) (uint64, error) 36 | Get(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order.Order, error) 37 | } 38 | 39 | // New instance of repository. 40 | func New(pool *pgxpool.Pool) *Repository { 41 | return &Repository{db: pool} 42 | } 43 | 44 | // Save new order to DB. 45 | func (r *Repository) Save(ctx context.Context, log logrus.FieldLogger, order *order_entity.Order) (uint64, error) { 46 | tx, err := r.db.BeginTx(ctx, pgx.TxOptions{}) 47 | if err != nil { 48 | return 0, fmt.Errorf("can't create tx: %s", err.Error()) 49 | } 50 | 51 | defer func() { 52 | if err != nil { 53 | rollbackErr := tx.Rollback(ctx) 54 | if rollbackErr != nil { 55 | err = baseErr.Join(err, rollbackErr) 56 | } 57 | } 58 | }() 59 | 60 | query, args, err := sq. 61 | Insert(ordersTable). 62 | Columns("user_id", "payment_type", "created_at"). 63 | Values( 64 | order.UserID, 65 | order.PaymentType, 66 | time.Now().Format(time.RFC3339), 67 | ). 68 | Suffix("RETURNING id"). 69 | PlaceholderFormat(sq.Dollar). 70 | ToSql() 71 | if err != nil { 72 | return 0, fmt.Errorf("can't build sql: %s", err.Error()) 73 | } 74 | 75 | // insert into orders table. 76 | rows, err := tx.Query(ctx, query, args...) 77 | if err != nil { 78 | return 0, fmt.Errorf("trx err: %w", err) 79 | } 80 | defer rows.Close() 81 | 82 | var orderID uint64 83 | for rows.Next() { 84 | if scanErr := rows.Scan(&orderID); scanErr != nil { 85 | return 0, fmt.Errorf("can't scan orderID: %s", scanErr.Error()) 86 | } 87 | } 88 | 89 | builder := sq. 90 | Insert(orderItemsTable). 91 | Columns( 92 | "order_id", 93 | "item_id", 94 | "original_amount", 95 | "discounted_amount", 96 | ) 97 | 98 | for _, service := range order.Items { 99 | builder = builder.Values( 100 | orderID, 101 | service.ID, 102 | service.Amount, 103 | service.DiscountedAmount) 104 | } 105 | 106 | query, args, err = builder.PlaceholderFormat(sq.Dollar).ToSql() 107 | if err != nil { 108 | return 0, fmt.Errorf("trx: %w", err) 109 | } 110 | 111 | // insert into services table. 112 | _, err = tx.Exec(ctx, query, args...) 113 | if err != nil { 114 | return 0, errors.Wrap(err, "txErr") 115 | } 116 | 117 | eventID, err := uuid.GenerateUUID() 118 | if err != nil { 119 | return 0, err 120 | } 121 | 122 | query, args, err = sq. 123 | Insert(OutboxTable). 124 | Columns("event_id", "order_id"). 125 | Values( 126 | eventID, 127 | orderID, 128 | ). 129 | PlaceholderFormat(sq.Dollar). 130 | ToSql() 131 | if err != nil { 132 | return 0, fmt.Errorf("can't build sql: %s", err.Error()) 133 | } 134 | 135 | if _, err := tx.Exec(ctx, query, args...); err != nil { 136 | return 0, fmt.Errorf("tx.Exec: %s", err.Error()) 137 | } 138 | 139 | if err := tx.Commit(ctx); err != nil { 140 | return 0, fmt.Errorf("can't commit tx: %s", err.Error()) 141 | } 142 | 143 | return orderID, nil 144 | } 145 | 146 | // Get returns map of orders. 147 | func (r *Repository) Get(ctx context.Context, log logrus.FieldLogger, IDs []uint64) (map[uint64]order.Order, error) { 148 | ordersMap := make(map[uint64]order.Order, len(IDs)) 149 | or := sq.Or{} 150 | orOrderItems := sq.Or{} 151 | for _, id := range IDs { 152 | or = append(or, sq.Eq{"id": id}) 153 | orOrderItems = append(orOrderItems, sq.Eq{"order_id": id}) 154 | } 155 | 156 | // build query. 157 | query, args, err := sq. 158 | Select("id", "user_id", "payment_type"). 159 | From(ordersTable). 160 | Where(or). 161 | PlaceholderFormat(sq.Dollar). 162 | ToSql() 163 | if err != nil { 164 | return nil, fmt.Errorf("can't build query: %s", err.Error()) 165 | } 166 | 167 | // get orders. 168 | rows, err := r.db.Query(ctx, query, args...) 169 | if err != nil { 170 | return nil, fmt.Errorf("can't select orders: %s", err.Error()) 171 | } 172 | defer rows.Close() 173 | 174 | for rows.Next() { 175 | ord := order.Order{} 176 | scanErr := rows.Scan(&ord.ID, &ord.UserID, &ord.PaymentType) 177 | if scanErr != nil { 178 | return nil, fmt.Errorf("can't scan order: %s", scanErr.Error()) 179 | } 180 | ordersMap[ord.ID] = ord 181 | } 182 | 183 | // build query 184 | query, args, err = squirrel. 185 | Select("order_id", "item_id", "original_amount", "discounted_amount"). 186 | From(orderItemsTable). 187 | Where(orOrderItems). 188 | PlaceholderFormat(sq.Dollar). 189 | ToSql() 190 | if err != nil { 191 | return nil, fmt.Errorf("can't build query") 192 | } 193 | 194 | // get order items 195 | rows, err = r.db.Query(ctx, query, args...) 196 | if err != nil { 197 | return nil, fmt.Errorf("can't select order_items: %s", err.Error()) 198 | } 199 | defer rows.Close() 200 | 201 | // put items in orders. 202 | for rows.Next() { 203 | service := order.Item{} 204 | err = rows.Scan(&service.OrderID, &service.ID, &service.Amount, &service.DiscountedAmount) 205 | 206 | if err != nil { 207 | return nil, fmt.Errorf("can't scan order: %s", err.Error()) 208 | } 209 | 210 | ord := ordersMap[service.OrderID] 211 | ord.Items = append(ord.Items, service) 212 | 213 | ordersMap[service.OrderID] = ord 214 | } 215 | return ordersMap, nil 216 | } 217 | -------------------------------------------------------------------------------- /internal/usecase/order/order.go: -------------------------------------------------------------------------------- 1 | package create_order 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Shopify/sarama" 8 | "github.com/ansakharov/lets_test/internal/pkg/entity/order" 9 | orderRepo "github.com/ansakharov/lets_test/internal/pkg/repository/order" 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const cursedUser = 666 15 | 16 | // Usecase responsible for saving request. 17 | type Usecase struct { 18 | repo orderRepo.OrderRepo 19 | producer sarama.SyncProducer 20 | } 21 | 22 | func New(orderRepo orderRepo.OrderRepo, producer sarama.SyncProducer) *Usecase { 23 | return &Usecase{ 24 | repo: orderRepo, 25 | producer: producer, 26 | } 27 | } 28 | 29 | // Save single order 30 | func (uc *Usecase) Save(ctx context.Context, log logrus.FieldLogger, order *order.Order) error { 31 | _, err := uc.repo.Save(ctx, log, order) 32 | if err != nil { 33 | return errors.Wrap(err, "repo.Save") 34 | } 35 | 36 | /*if order.UserID == cursedUser { 37 | return errors.New("some err") 38 | } 39 | 40 | if _, _, err = uc.producer.SendMessage(&sarama.ProducerMessage{ 41 | Topic: kafka.Topic, 42 | Value: sarama.StringEncoder(fmt.Sprintf("{order_id:%d}", orderID)), 43 | }); err != nil { 44 | return errors.Wrap(err, "producer.SendMessage") 45 | }*/ 46 | 47 | // tx.Commit() 48 | 49 | return nil 50 | } 51 | 52 | // Get orders by ids 53 | func (uc *Usecase) Get(ctx context.Context, log logrus.FieldLogger, IDs []uint64) ([]order.Order, error) { 54 | ordersMap, err := uc.repo.Get(ctx, log, IDs) 55 | if err != nil { 56 | return nil, fmt.Errorf("err from orders_repository: %s", err.Error()) 57 | } 58 | 59 | // count amount and discount for all orders. 60 | for idx, singleOrder := range ordersMap { 61 | for _, singleService := range singleOrder.Items { 62 | singleOrder.OriginalAmount += singleService.Amount 63 | singleOrder.DiscountedAmount += singleService.DiscountedAmount 64 | ordersMap[idx] = singleOrder 65 | } 66 | } 67 | 68 | result := make([]order.Order, 0, len(ordersMap)) 69 | for _, ord := range ordersMap { 70 | result = append(result, ord) 71 | } 72 | 73 | return result, nil 74 | } 75 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func New() *logrus.Logger { 10 | return &logrus.Logger{ 11 | Out: os.Stderr, 12 | Formatter: new(logrus.TextFormatter), 13 | Hooks: make(logrus.LevelHooks), 14 | Level: logrus.DebugLevel, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /migration/01_create_orders.sql: -------------------------------------------------------------------------------- 1 | create table if not exists items ( 2 | id bigserial PRIMARY KEY, 3 | name text, 4 | price integer 5 | ); 6 | 7 | create table if not exists orders ( 8 | id bigserial PRIMARY KEY, 9 | user_id integer, 10 | payment_type smallint, 11 | created_at timestamptz 12 | ); 13 | 14 | create table if not exists order_items ( 15 | order_item_id bigserial PRIMARY KEY, 16 | order_id bigint, 17 | item_id bigint, 18 | original_amount integer, 19 | discounted_amount integer, 20 | 21 | CONSTRAINT fk_order_item_id 22 | FOREIGN KEY(order_id) 23 | REFERENCES orders(id), 24 | 25 | CONSTRAINT fk_items 26 | FOREIGN KEY(item_id) 27 | REFERENCES items(id) 28 | ); 29 | 30 | insert into items (name, price) VALUES 31 | ('premium', 100000), 32 | ('calltracking', 20000), 33 | ('autoload', 200000), 34 | ('limit', 500000); 35 | -------------------------------------------------------------------------------- /migration/02_outbox_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE outbox ( 2 | id SERIAL PRIMARY KEY, 3 | event_id TEXT NOT NULL, 4 | order_id INTEGER NOT NULL, 5 | sent BOOLEAN DEFAULT FALSE 6 | ); 7 | --------------------------------------------------------------------------------