├── .dockerignore ├── .gitignore ├── Makefile ├── Readme.md ├── chat ├── cmd │ └── main.go ├── go.mod ├── go.sum └── internal │ ├── client.go │ └── hub.go ├── configs ├── grafana │ └── provisioning │ │ └── datasources │ │ ├── loki.yaml │ │ └── prometheus.yaml ├── loki │ └── config.yaml ├── prometheus │ └── prometheus.yaml └── vector │ └── vector.toml ├── customer-gateway ├── cmd │ └── main.go ├── go.mod ├── go.sum └── internal │ ├── graphql │ ├── generated.go │ ├── gqlgen.yaml │ ├── graphql.go │ ├── models_gen.go │ ├── mutation_resolver.go │ ├── query_resolver.go │ └── schema.graphql │ └── grpc │ ├── grpc.go │ └── pb │ ├── customer.pb.go │ └── customer_grpc.pb.go ├── customer ├── .env.example ├── Makefile ├── cmd │ └── main.go ├── configs │ ├── app.example │ │ └── prod.example.toml │ ├── app │ │ ├── dev.toml │ │ └── prod.toml │ └── db │ │ └── init.sql ├── go.mod ├── go.sum ├── graph.gv ├── graph.png ├── graph.svg ├── internal │ ├── application │ │ ├── commands │ │ │ ├── commands.go │ │ │ ├── create_customer.go │ │ │ ├── create_test.go │ │ │ ├── dto.go │ │ │ ├── mocks.go │ │ │ ├── service_commands.go │ │ │ └── upload_avatar.go │ │ ├── persistence │ │ │ └── persistence.go │ │ └── services.go │ ├── domain │ │ ├── aggregate │ │ │ ├── aggregate.go │ │ │ ├── aggregate_methods.go │ │ │ ├── aggregate_test.go │ │ │ ├── command.go │ │ │ ├── store.go │ │ │ └── utils.go │ │ ├── common │ │ │ ├── aggregate.go │ │ │ ├── aggregate_methods.go │ │ │ ├── event.go │ │ │ ├── event_methods.go │ │ │ └── exceptions.go │ │ ├── consts │ │ │ └── transaction.go │ │ ├── entities │ │ │ └── customerTransactions.go │ │ ├── events │ │ │ └── events.go │ │ ├── exceptions │ │ │ ├── email.go │ │ │ ├── fullname.go │ │ │ └── store.go │ │ └── vo │ │ │ ├── balanceMoney.go │ │ │ ├── customerBalance.go │ │ │ ├── email.go │ │ │ ├── email_test.go │ │ │ ├── fullName.go │ │ │ └── fullname_test.go │ ├── infrastructure │ │ ├── db │ │ │ ├── config.go │ │ │ ├── convertors.go │ │ │ ├── db.go │ │ │ ├── migrations │ │ │ │ ├── 000001_init.down.sql │ │ │ │ ├── 000001_init.up.sql │ │ │ │ ├── 000002_creaed_at_field.down.sql │ │ │ │ ├── 000002_creaed_at_field.up.sql │ │ │ │ ├── 000003_outbox.down.sql │ │ │ │ └── 000003_outbox.up.sql │ │ │ ├── models.go │ │ │ ├── repo.go │ │ │ └── uow.go │ │ ├── di │ │ │ └── di.go │ │ └── minio │ │ │ └── minio.go │ └── presentation │ │ ├── api │ │ ├── api.go │ │ ├── docs │ │ │ ├── docs.go │ │ │ ├── swagger.json │ │ │ └── swagger.yaml │ │ ├── dto.go │ │ ├── engine.go │ │ ├── group.go │ │ ├── handlers.go │ │ ├── main.go │ │ ├── middlewares.go │ │ └── routes.go │ │ ├── graph │ │ └── graph.go │ │ └── grpc │ │ ├── grpc.go │ │ ├── pb │ │ ├── customer.pb.go │ │ └── customer_grpc.pb.go │ │ ├── server.go │ │ └── service.go └── protobuf │ ├── customer.proto │ └── servicespb │ ├── customer.pb.go │ └── customer_grpc.pb.go ├── docker-compose.yaml ├── order ├── .env.example ├── Dockerfile ├── Makefile ├── cmd │ └── main.go ├── configs │ ├── app.example │ │ └── prod.example.toml │ ├── app │ │ ├── dev.toml │ │ └── prod.toml │ └── db │ │ └── init.sql ├── go.mod ├── go.sum ├── graph.gv ├── graph.png ├── graph.svg └── internal │ ├── application │ ├── common │ │ ├── consts │ │ │ ├── outbox │ │ │ │ └── outbox.go │ │ │ └── saga.go │ │ ├── dto │ │ │ └── sequence.go │ │ └── interfaces │ │ │ ├── broker │ │ │ └── broker.go │ │ │ ├── logger │ │ │ └── logger.go │ │ │ └── persistence │ │ │ ├── filters │ │ │ └── filters.go │ │ │ ├── query │ │ │ └── query.go │ │ │ ├── repo │ │ │ └── outbox.go │ │ │ └── uow.go │ ├── order │ │ ├── cache │ │ │ └── order_create.go │ │ ├── command │ │ │ ├── create_order.go │ │ │ └── delete_order.go │ │ ├── dto │ │ │ ├── order.go │ │ │ └── products.go │ │ ├── exceptions │ │ │ └── order.go │ │ ├── interfaces │ │ │ ├── cache │ │ │ │ └── order_create.go │ │ │ ├── command │ │ │ │ ├── create_order.go │ │ │ │ └── delete_order.go │ │ │ ├── persistence │ │ │ │ ├── dao │ │ │ │ │ └── dao.go │ │ │ │ ├── filters │ │ │ │ │ └── filters.go │ │ │ │ ├── reader │ │ │ │ │ └── order.go │ │ │ │ └── repo │ │ │ │ │ └── order.go │ │ │ ├── query │ │ │ │ ├── get_all_orders.go │ │ │ │ ├── get_all_orders_by_user.go │ │ │ │ └── get_order_by_id.go │ │ │ └── saga │ │ │ │ └── saga.go │ │ ├── query │ │ │ ├── get_all_orders.go │ │ │ ├── get_all_orders_by_user.go │ │ │ └── get_order_by_id.go │ │ └── saga │ │ │ └── saga.go │ ├── product │ │ ├── command │ │ │ ├── create_product.go │ │ │ └── update_product_name.go │ │ ├── dto │ │ │ └── product.go │ │ ├── exceptions │ │ │ └── product.go │ │ ├── interfaces │ │ │ ├── command │ │ │ │ ├── create_product.go │ │ │ │ └── update_product_name.go │ │ │ ├── persistence │ │ │ │ ├── dao │ │ │ │ │ └── product.go │ │ │ │ ├── filters │ │ │ │ │ └── filters.go │ │ │ │ └── reader │ │ │ │ │ └── product.go │ │ │ └── query │ │ │ │ ├── get_all_products.go │ │ │ │ └── get_product_by_name.go │ │ └── query │ │ │ ├── get_all_products.go │ │ │ └── get_product_by_name.go │ └── relay │ │ ├── dto │ │ └── outbox.go │ │ ├── interactors │ │ └── relay.go │ │ └── interfaces │ │ ├── interactors │ │ └── relay.go │ │ └── persistence │ │ └── dao │ │ └── outbox.go │ ├── domain │ ├── common │ │ ├── aggregate │ │ │ └── aggregate.go │ │ ├── events │ │ │ └── events.go │ │ ├── exceptions.go │ │ └── id │ │ │ └── id.go │ ├── order │ │ ├── aggregate │ │ │ ├── aggregate.go │ │ │ ├── mutation.go │ │ │ ├── read.go │ │ │ └── validate.go │ │ ├── consts │ │ │ └── consts.go │ │ ├── entities │ │ │ ├── orderAdress.go │ │ │ ├── orderClient.go │ │ │ └── orderProduct.go │ │ ├── events │ │ │ ├── create_order.go │ │ │ ├── order_delete.go │ │ │ ├── product_order_add.go │ │ │ └── saga_event.go │ │ ├── exceptions │ │ │ └── exceptions.go │ │ ├── services │ │ │ └── createOrder.go │ │ └── vo │ │ │ ├── order_delete.go │ │ │ ├── order_id.go │ │ │ └── order_info.go │ └── product │ │ ├── entities │ │ └── product.go │ │ ├── exceptions │ │ └── product.go │ │ └── vo │ │ ├── product_discount.go │ │ ├── product_id.go │ │ └── product_price.go │ ├── infrastructure │ ├── cache │ │ ├── cache.go │ │ ├── config │ │ │ └── config.go │ │ ├── dao │ │ │ ├── dao.go │ │ │ └── order │ │ │ │ └── cache.go │ │ └── reader │ │ │ ├── order │ │ │ └── cache.go │ │ │ └── reader.go │ ├── config │ │ └── load-config.go │ ├── db │ │ ├── config │ │ │ └── config.go │ │ ├── dao │ │ │ ├── base.go │ │ │ ├── order │ │ │ │ ├── convertors.go │ │ │ │ ├── order.go │ │ │ │ └── saga.go │ │ │ ├── outbox │ │ │ │ ├── convertors.go │ │ │ │ └── outbox.go │ │ │ └── product │ │ │ │ ├── convertors.go │ │ │ │ └── product.go │ │ ├── db.go │ │ ├── migrations.go │ │ ├── models │ │ │ ├── base.go │ │ │ ├── order.go │ │ │ ├── outbox.go │ │ │ └── product.go │ │ ├── reader │ │ │ ├── product │ │ │ │ ├── convertors.go │ │ │ │ └── product.go │ │ │ └── utils.go │ │ ├── repo │ │ │ ├── base.go │ │ │ ├── order │ │ │ │ ├── convertors.go │ │ │ │ └── order.go │ │ │ └── outbox │ │ │ │ ├── convertors │ │ │ │ ├── convert_order.go │ │ │ │ └── convertors.go │ │ │ │ └── outbox.go │ │ └── uow │ │ │ └── uow.go │ ├── di │ │ ├── di.go │ │ └── factories │ │ │ ├── cache │ │ │ ├── cache.go │ │ │ ├── dao │ │ │ │ └── order.go │ │ │ └── reader │ │ │ │ └── order.go │ │ │ ├── db │ │ │ ├── db.go │ │ │ ├── orders │ │ │ │ └── order.go │ │ │ ├── outbox │ │ │ │ └── outbox.go │ │ │ └── product │ │ │ │ └── product.go │ │ │ ├── interactors │ │ │ ├── interactors.go │ │ │ ├── order │ │ │ │ └── order.go │ │ │ ├── product │ │ │ │ └── product.go │ │ │ └── relay │ │ │ │ └── relay.go │ │ │ ├── logger │ │ │ └── logger.go │ │ │ ├── mediator │ │ │ ├── mediator.go │ │ │ └── params.go │ │ │ └── messageBroker │ │ │ └── message_broker.go │ ├── logger │ │ ├── config │ │ │ └── config.go │ │ ├── fx.go │ │ ├── gin.go │ │ ├── gorm.go │ │ └── logger.go │ ├── mediator │ │ ├── dispatchers │ │ │ ├── commandDispatcher │ │ │ │ └── command_dispatcher.go │ │ │ └── queryDispatcher │ │ │ │ └── query_dispatcher.go │ │ ├── mediator.go │ │ ├── orders.go │ │ └── product.go │ └── messageBroker │ │ ├── brokerConfigurate │ │ ├── broker_configurate.go │ │ ├── interfaces │ │ │ └── interfaces.go │ │ ├── order │ │ │ └── order.go │ │ └── setup.go │ │ ├── config │ │ └── config.go │ │ ├── controller │ │ └── controller.go │ │ └── message_broker.go │ └── presentation │ ├── api │ ├── api.go │ ├── config │ │ └── config.go │ ├── controllers │ │ ├── handlers │ │ │ ├── healthcheck │ │ │ │ ├── handler.go │ │ │ │ └── healthcheck.go │ │ │ ├── order │ │ │ │ ├── handler.go │ │ │ │ └── order.go │ │ │ ├── params.go │ │ │ └── product │ │ │ │ ├── handler.go │ │ │ │ └── product.go │ │ ├── response │ │ │ └── exceptions.go │ │ └── routes │ │ │ ├── group.go │ │ │ ├── healthcheck │ │ │ └── healthcheck.go │ │ │ ├── order │ │ │ └── order.go │ │ │ ├── product │ │ │ └── product.go │ │ │ └── routes.go │ ├── engine │ │ └── engine.go │ ├── middleware │ │ ├── errorHandler │ │ │ ├── errro_handling.go │ │ │ ├── order.go │ │ │ └── product.go │ │ ├── interfaces │ │ │ └── interfaces.go │ │ ├── logging │ │ │ └── logging.go │ │ └── middleware.go │ └── prometheus │ │ └── prometheus.go │ ├── config │ ├── config.go │ └── factories.go │ ├── consumer │ ├── consumer.go │ └── subscribers │ │ ├── order │ │ ├── events.go │ │ └── saga.go │ │ └── subscribers.go │ ├── cron │ ├── config │ │ └── config.go │ ├── cron.go │ ├── engine │ │ └── engine.go │ └── handlers │ │ ├── relay │ │ └── relay.go │ │ └── setup.go │ ├── di │ ├── api │ │ ├── api.go │ │ ├── controllers │ │ │ ├── controllers.go │ │ │ ├── handlers │ │ │ │ └── handlers.go │ │ │ └── routes │ │ │ │ └── routes.go │ │ ├── engine │ │ │ └── engine.go │ │ └── middleware │ │ │ └── middleware.go │ ├── config │ │ └── config.go │ ├── consumer │ │ └── consumer.go │ └── cron │ │ └── cron.go │ └── graph │ └── graph.go └── pkg ├── env ├── env.go └── go.mod └── rabbit ├── channel.go ├── connection.go ├── examples └── main.go ├── go.mod ├── go.sum ├── interfaces.go └── pool.go /.dockerignore: -------------------------------------------------------------------------------- 1 | dump 2 | dump-test 3 | /order/dump-postgres 4 | /order/dump-rabbit_mq 5 | /order/dump-redis_data 6 | tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dump 3 | dump-test 4 | dump-container-test 5 | .wakatime-project 6 | .env.dev 7 | .test.env 8 | .test.container.env 9 | /configs/app 10 | tests 11 | docker-compose.test.yaml 12 | docker-compose.dev.yaml 13 | DockerfileTest 14 | .env 15 | 16 | 17 | dump-postgres 18 | dump-rabbit_mq 19 | dump-redis_data -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | up-prod: 2 | docker-compose -p prod -f ./docker-compose.yaml up --build -------------------------------------------------------------------------------- /chat/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/chat/internal" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | var addr = flag.String("addr", ":8080", "http service address") 12 | 13 | func main() { 14 | flag.Parse() 15 | hub := internal.NewHub() 16 | go hub.Run() 17 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 18 | internal.ServeWs(hub, w, r) 19 | }) 20 | server := &http.Server{ 21 | Addr: *addr, 22 | ReadHeaderTimeout: 3 * time.Second, 23 | } 24 | err := server.ListenAndServe() 25 | if err != nil { 26 | log.Fatal("ListenAndServe: ", err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chat/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MikhailGulkin/simpleGoOrderApp/chat 2 | 3 | go 1.21.1 4 | 5 | require github.com/gorilla/websocket v1.5.0 6 | -------------------------------------------------------------------------------- /chat/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 2 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | -------------------------------------------------------------------------------- /chat/internal/hub.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type Hub struct { 4 | // Registered clients. 5 | clients map[*Client]bool 6 | 7 | // Inbound messages from the clients. 8 | broadcast chan []byte 9 | 10 | // Register requests from the clients. 11 | register chan *Client 12 | 13 | // Unregister requests from clients. 14 | unregister chan *Client 15 | } 16 | 17 | func NewHub() *Hub { 18 | return &Hub{ 19 | broadcast: make(chan []byte), 20 | register: make(chan *Client), 21 | unregister: make(chan *Client), 22 | clients: make(map[*Client]bool), 23 | } 24 | } 25 | 26 | func (h *Hub) Run() { 27 | for { 28 | select { 29 | case client := <-h.register: 30 | h.clients[client] = true 31 | case client := <-h.unregister: 32 | if _, ok := h.clients[client]; ok { 33 | delete(h.clients, client) 34 | close(client.send) 35 | } 36 | case message := <-h.broadcast: 37 | for client := range h.clients { 38 | select { 39 | case client.send <- message: 40 | default: 41 | close(client.send) 42 | delete(h.clients, client) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /configs/grafana/provisioning/datasources/loki.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Loki 5 | type: loki 6 | access: proxy 7 | url: http://orderService.loki:3100 8 | basicAuth: false 9 | isDefault: true 10 | editable: true 11 | orgId: 1 12 | version: 1 13 | jsonData: 14 | timeInterval: 15s 15 | -------------------------------------------------------------------------------- /configs/grafana/provisioning/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | url: http://orderService.prometheus:9090 8 | basicAuth: false 9 | isDefault: false 10 | editable: true 11 | orgId: 1 12 | version: 1 13 | -------------------------------------------------------------------------------- /configs/loki/config.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | grpc_listen_port: 9096 6 | 7 | common: 8 | path_prefix: /tmp/loki 9 | storage: 10 | filesystem: 11 | chunks_directory: /tmp/loki/chunks 12 | rules_directory: /tmp/loki/rules 13 | replication_factor: 1 14 | ring: 15 | instance_addr: 127.0.0.1 16 | kvstore: 17 | store: inmemory 18 | 19 | schema_config: 20 | configs: 21 | - from: 2023-01-01 22 | store: boltdb-shipper 23 | object_store: filesystem 24 | schema: v11 25 | index: 26 | prefix: index_ 27 | period: 24h 28 | 29 | query_range: 30 | results_cache: 31 | cache: 32 | embedded_cache: 33 | enabled: true 34 | max_size_mb: 100 35 | 36 | ruler: 37 | alertmanager_url: http://localhost:9093 -------------------------------------------------------------------------------- /configs/prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 5s # By default, scrape targets every 15 seconds. 4 | 5 | scrape_configs: 6 | - job_name: 'orderService' 7 | static_configs: 8 | - targets: [ "orderService.api:8000" ] 9 | 10 | - job_name: "prometheus" 11 | static_configs: 12 | - targets: [ "localhost:9090" ] -------------------------------------------------------------------------------- /configs/vector/vector.toml: -------------------------------------------------------------------------------- 1 | [sources.docker] 2 | type = "docker_logs" 3 | docker_host = "/var/run/docker.sock" 4 | include_containers = [ "orderService.redis", "orderService.api", "orderService.postgres", "orderService.rabbitmq"] 5 | 6 | [transforms.json] 7 | type = "remap" 8 | inputs = ["docker"] 9 | drop_on_error = true 10 | source = ".message = object!(parse_json(.message) ?? {})" 11 | 12 | [sinks.console] 13 | type = "console" 14 | inputs = ["json"] 15 | encoding.codec = "json" 16 | 17 | [sinks.loki_sync_id] 18 | type = "loki" 19 | inputs = ["json"] 20 | encoding.codec = "json" 21 | labels.event = "log" 22 | labels.container_name = "{{container_name}}" 23 | endpoint = "http://orderService.loki:3100" -------------------------------------------------------------------------------- /customer-gateway/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/99designs/gqlgen/handler" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env" 7 | "github.com/MikhailGulkin/simpleGoOrderApp/customer-gateway/internal/graphql" 8 | "github.com/MikhailGulkin/simpleGoOrderApp/customer-gateway/internal/grpc" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func main() { 14 | grpcClient, err := grpc.NewClient(env.GetEnv("GRPC_CLIENT", "localhost:50052")) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | s, err := graphql.NewGraphQLServer(grpcClient) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | http.Handle("/graphql", handler.GraphQL(s.ToExecutableSchema())) 24 | 25 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", env.GetEnv("GRAPH_PORT", "8001")), nil)) 26 | } 27 | -------------------------------------------------------------------------------- /customer-gateway/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MikhailGulkin/simpleGoOrderApp/customer-gateway 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.17.36 7 | github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env v0.0.0-20230825192009-3a1d4df0af00 8 | github.com/vektah/gqlparser/v2 v2.5.8 9 | google.golang.org/grpc v1.57.0 10 | google.golang.org/protobuf v1.30.0 11 | ) 12 | 13 | require ( 14 | github.com/agnivade/levenshtein v1.1.1 // indirect 15 | github.com/golang/protobuf v1.5.3 // indirect 16 | github.com/gorilla/websocket v1.5.0 // indirect 17 | github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect 18 | github.com/mitchellh/mapstructure v1.5.0 // indirect 19 | golang.org/x/net v0.9.0 // indirect 20 | golang.org/x/sys v0.8.0 // indirect 21 | golang.org/x/text v0.9.0 // indirect 22 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /customer-gateway/internal/graphql/gqlgen.yaml: -------------------------------------------------------------------------------- 1 | schema: schema.graphql -------------------------------------------------------------------------------- /customer-gateway/internal/graphql/graphql.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/99designs/gqlgen generate 2 | package graphql 3 | 4 | import ( 5 | "github.com/99designs/gqlgen/graphql" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer-gateway/internal/grpc/pb" 7 | ) 8 | 9 | type Server struct { 10 | customerClient pb.CustomerServiceClient 11 | } 12 | 13 | func NewGraphQLServer(client pb.CustomerServiceClient) (*Server, error) { 14 | return &Server{ 15 | customerClient: client, 16 | }, nil 17 | } 18 | func (s *Server) Mutation() MutationResolver { 19 | return &mutationResolver{ 20 | client: s.customerClient, 21 | } 22 | } 23 | func (s *Server) Query() QueryResolver { 24 | return &queryResolver{} 25 | } 26 | func (s *Server) ToExecutableSchema() graphql.ExecutableSchema { 27 | return NewExecutableSchema(Config{ 28 | Resolvers: s, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /customer-gateway/internal/graphql/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package graphql 4 | 5 | type CustomerInput struct { 6 | ID string `json:"id"` 7 | FirstName string `json:"firstName"` 8 | MiddleName string `json:"middleName"` 9 | LastName string `json:"lastName"` 10 | Email string `json:"email"` 11 | AddressID string `json:"addressID"` 12 | } 13 | 14 | type CustomerResponse struct { 15 | ID string `json:"id"` 16 | Event string `json:"event"` 17 | } 18 | 19 | type SomeQuery struct { 20 | ID string `json:"id"` 21 | } 22 | -------------------------------------------------------------------------------- /customer-gateway/internal/graphql/mutation_resolver.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "context" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer-gateway/internal/grpc/pb" 6 | "time" 7 | ) 8 | 9 | type mutationResolver struct { 10 | client pb.CustomerServiceClient 11 | } 12 | 13 | func (r *mutationResolver) CreateCustomer(ctx context.Context, input CustomerInput) (*CustomerResponse, error) { 14 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 15 | defer cancel() 16 | 17 | customer, err := r.client.CreateCustomer(ctx, &pb.CreateCustomerRequest{ 18 | CustomerID: input.ID, 19 | FirstName: input.FirstName, 20 | MiddleName: input.MiddleName, 21 | LastName: input.LastName, 22 | Email: input.Email, 23 | AddressID: input.AddressID, 24 | }) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &CustomerResponse{ 29 | customer.Id, 30 | customer.EventID, 31 | }, nil 32 | } 33 | -------------------------------------------------------------------------------- /customer-gateway/internal/graphql/query_resolver.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import "context" 4 | 5 | type queryResolver struct { 6 | } 7 | 8 | func (q *queryResolver) GetCustomer(ctx context.Context, id SomeQuery) (*CustomerResponse, error) { 9 | panic("not implemented") 10 | } 11 | -------------------------------------------------------------------------------- /customer-gateway/internal/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | input CustomerInput { 2 | id: ID! 3 | firstName: String! 4 | middleName: String! 5 | lastName: String! 6 | email: String! 7 | addressID: String! 8 | } 9 | type CustomerResponse { 10 | id: ID! 11 | event: String! 12 | } 13 | input SomeQuery { 14 | id: ID! 15 | } 16 | type Query { 17 | getCustomer(id: SomeQuery!): CustomerResponse 18 | } 19 | type Mutation { 20 | createCustomer(customer: CustomerInput!): CustomerResponse 21 | } -------------------------------------------------------------------------------- /customer-gateway/internal/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer-gateway/internal/grpc/pb" 5 | "google.golang.org/grpc" 6 | "google.golang.org/grpc/credentials/insecure" 7 | ) 8 | 9 | func NewClient(url string) (pb.CustomerServiceClient, error) { 10 | conn, err := grpc.Dial(url, grpc.WithTransportCredentials(insecure.NewCredentials())) 11 | if err != nil { 12 | return nil, err 13 | } 14 | c := pb.NewCustomerServiceClient(conn) 15 | return c, nil 16 | } 17 | -------------------------------------------------------------------------------- /customer/.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=OrderApp 2 | POSTGRES_USER=postgres 3 | POSTGRES_PASSWORD=1234 4 | POSTGRES_HOST=localhost 5 | POSTGRES_PORT=5432 6 | 7 | 8 | RABBITMQ_DEFAULT_USER='admin' 9 | RABBITMQ_DEFAULT_PASS='admin' 10 | 11 | GF_SECURITY_ADMIN_USER='admin' 12 | GF_SECURITY_ADMIN_PASSWORD='admin' -------------------------------------------------------------------------------- /customer/Makefile: -------------------------------------------------------------------------------- 1 | include .env.dev 2 | up-containers: 3 | docker-compose -p customer-app -f ./docker-compose.dev.yaml up --build 4 | migrate-new: 5 | migrate create -ext sql -dir ${MIGRATION_PATH} -seq ${MIGRATION_NAME} 6 | migrate-up: 7 | migrate -path ${MIGRATION_PATH} -database ${DB_URL} -verbose up 8 | migrate-down: 9 | migrate -path ${MIGRATION_PATH} -database ${DB_URL} -verbose down 10 | migrate-force: 11 | migrate -path ${MIGRATION_PATH} -database ${DB_URL} -verbose force ${VERSION} 12 | generate-customer: 13 | cd protobuf && \ 14 | protoc --go_out=. \ 15 | --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false \ 16 | customer.proto 17 | -------------------------------------------------------------------------------- /customer/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/infrastructure/di" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/presentation/api" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/presentation/graph" 7 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/presentation/grpc" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | func main() { 12 | fx.New( 13 | di.Module, 14 | graph.Module, 15 | grpc.Module, 16 | api.Module, 17 | ).Run() 18 | } 19 | -------------------------------------------------------------------------------- /customer/configs/app.example/prod.example.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | mode='production' 3 | 4 | [api] 5 | host = "0.0.0.0" 6 | port = 8000 7 | base_url_prefix = "/api/v1" 8 | 9 | [db] 10 | host = "orderService.postgres" 11 | port = 5432 12 | database = "OrderApp" 13 | user = "postgres" 14 | password = "1234" 15 | migration = true 16 | logging = true 17 | max_idle_connection = 100 18 | 19 | [broker] 20 | host = "orderService.rabbitmq" 21 | port = 5672 22 | login = "admin" 23 | password = "admin" 24 | 25 | [cron] 26 | seconds = 5 27 | 28 | [logging] 29 | log_output='' 30 | log_level='info' 31 | 32 | [cache] 33 | host="orderService.redis" 34 | port=6379 35 | password="" 36 | db=0 -------------------------------------------------------------------------------- /customer/configs/app/dev.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | mode='development' 3 | 4 | [api] 5 | host = "0.0.0.0" 6 | port = 8000 7 | base_url_prefix = "/api/v1" 8 | 9 | [db] 10 | host = "localhost" 11 | port = 5431 12 | database = "CustomerApp" 13 | user = "postgres" 14 | password = "1234" 15 | migration = true 16 | logging = true 17 | max_idle_connection = 100 18 | 19 | [broker] 20 | host = "localhost" 21 | port = 5672 22 | login = "admin" 23 | password = "admin" 24 | 25 | [cron] 26 | seconds = 5 27 | 28 | [logging] 29 | log_output='' 30 | log_level='info' 31 | 32 | [cache] 33 | host="localhost" 34 | port=6379 35 | password="" 36 | db=0 -------------------------------------------------------------------------------- /customer/configs/app/prod.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | mode='production' 3 | 4 | [api] 5 | host = "0.0.0.0" 6 | port = 8000 7 | base_url_prefix = "/api/v1" 8 | 9 | [db] 10 | host = "orderService.postgres" 11 | port = 5432 12 | database = "OrderApp" 13 | user = "postgres" 14 | password = "1234" 15 | migration = true 16 | logging = true 17 | max_idle_connection = 100 18 | 19 | [broker] 20 | host = "orderService.rabbitmq" 21 | port = 5672 22 | login = "admin" 23 | password = "admin" 24 | 25 | [cron] 26 | seconds = 5 27 | 28 | [logging] 29 | log_output='' 30 | log_level='info' 31 | 32 | [cache] 33 | host="orderService.redis" 34 | port=6379 35 | password="" 36 | db=0 -------------------------------------------------------------------------------- /customer/configs/db/init.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | -------------------------------------------------------------------------------- /customer/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikhailGulkin/CleanGolangOrderApp/605b38bc0e6d3d2df01b0421311773f3a60c0073/customer/graph.png -------------------------------------------------------------------------------- /customer/internal/application/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type CreateCustomerCommand struct { 8 | CustomerID uuid.UUID `json:"customerID" binding:"required"` 9 | FirstName string `json:"firstName" binding:"required"` 10 | LastName string `json:"lastName" binding:"required"` 11 | MiddleName string `json:"middleName" binding:"required"` 12 | AddressID uuid.UUID `json:"addressID" binding:"required"` 13 | Email string `json:"email" binding:"required"` 14 | } 15 | type UploadAvatarCommand struct { 16 | CustomerID uuid.UUID `json:"customerID" binding:"required"` 17 | Avatar []byte `json:"avatar" binding:"required"` 18 | } 19 | -------------------------------------------------------------------------------- /customer/internal/application/commands/create_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/go-faker/faker/v4" 5 | "github.com/stretchr/testify/mock" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestSuccessCreateCustomerHandle(t *testing.T) { 11 | createCustomerCommand := CreateCustomerCommand{} 12 | if err := faker.FakeData(&createCustomerCommand); err != nil { 13 | t.Fatal(err) 14 | } 15 | eventStore := new(MockEventStore) 16 | eventStore.On("Exists", mock.Anything).Return(nil) 17 | eventStore.On("Create", mock.Anything, mock.Anything).Return(nil) 18 | 19 | createCustomerCommand.FirstName = "Nike" 20 | createCustomerCommand.MiddleName = "Vladimirovich" 21 | createCustomerCommand.LastName = "Sulkin" 22 | 23 | createCustomer := NewCreateCustomerHandler(eventStore, &TestOutbox{}, &TestUoWManager{}) 24 | 25 | customerDTO, err := createCustomer.Handle(createCustomerCommand) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if strings.Contains(customerDTO.CustomerID, createCustomerCommand.CustomerID.String()) == false { 30 | t.Fatalf("expected customerID: %s, got: %s", createCustomerCommand.CustomerID, customerDTO.CustomerID) 31 | } 32 | if customerDTO.EventID == "" { 33 | t.Fatalf("expected eventID: %s, got: %s", "", customerDTO.EventID) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /customer/internal/application/commands/dto.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | type CustomerCreateDTO struct { 4 | CustomerID string `json:"customerID" binding:"required"` 5 | EventID string `json:"eventID" binding:"required"` 6 | } 7 | 8 | type UploadAvatarDTO struct { 9 | AvatarUri string `json:"avatarUri" binding:"required"` 10 | CustomerID string `json:"customerID" binding:"required"` 11 | EventID string `json:"eventID" binding:"required"` 12 | } 13 | -------------------------------------------------------------------------------- /customer/internal/application/commands/mocks.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/persistence" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/common" 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type MockEventStore struct { 10 | mock.Mock 11 | } 12 | 13 | func (t *MockEventStore) Create(aggregate common.Aggregate, tx interface{}) error { 14 | args := t.Called(aggregate, tx) 15 | return args.Error(0) 16 | } 17 | func (t *MockEventStore) Update(aggregate common.Aggregate, tx interface{}) error { 18 | args := t.Called(aggregate, tx) 19 | return args.Error(0) 20 | } 21 | func (t *MockEventStore) Load(aggregate common.Aggregate) error { 22 | args := t.Called(aggregate) 23 | return args.Error(0) 24 | } 25 | 26 | func (t *MockEventStore) Exists(id string) error { 27 | args := t.Called(id) 28 | return args.Error(0) 29 | } 30 | 31 | type TestOutbox struct { 32 | } 33 | 34 | func (t *TestOutbox) AddEvents(_ []common.Event, _ interface{}) error { 35 | return nil 36 | } 37 | 38 | type TestUoW struct { 39 | } 40 | 41 | func (t *TestUoW) Begin() (interface{}, error) { 42 | return nil, nil 43 | } 44 | func (t *TestUoW) Commit() error { 45 | return nil 46 | } 47 | func (t *TestUoW) Rollback() error { 48 | return nil 49 | } 50 | 51 | type TestUoWManager struct { 52 | } 53 | 54 | func (t *TestUoWManager) GetUoW() persistence.UoW { 55 | return &TestUoW{} 56 | } 57 | -------------------------------------------------------------------------------- /customer/internal/application/commands/service_commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | type CustomerCommands struct { 4 | CreateCustomer *CreateCustomerHandler `json:"createCustomer,omitempty"` 5 | UploadCustomerAvatar *UploadAvatarCustomerHandler `json:"uploadCustomerAvatar,omitempty"` 6 | } 7 | 8 | func NewCustomerCommands( 9 | createCustomer *CreateCustomerHandler, 10 | uploadCustomer *UploadAvatarCustomerHandler, 11 | ) *CustomerCommands { 12 | return &CustomerCommands{ 13 | CreateCustomer: createCustomer, 14 | UploadCustomerAvatar: uploadCustomer, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /customer/internal/application/persistence/persistence.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "context" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/common" 6 | ) 7 | 8 | type Bucket interface { 9 | UploadAvatar(ctx context.Context, name string, fileBuffer []byte) error 10 | } 11 | type Outbox interface { 12 | AddEvents(events []common.Event, tx interface{}) error 13 | } 14 | type UoW interface { 15 | Commit() error 16 | Rollback() error 17 | Begin() (interface{}, error) 18 | } 19 | type UoWManager interface { 20 | GetUoW() UoW 21 | } 22 | -------------------------------------------------------------------------------- /customer/internal/application/services.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/commands" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/persistence" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/aggregate" 7 | ) 8 | 9 | type CustomerServices struct { 10 | Commands *commands.CustomerCommands 11 | } 12 | 13 | func NewCustomerServices( 14 | es aggregate.EventStore, 15 | outbox persistence.Outbox, 16 | manager persistence.UoWManager, 17 | bucket persistence.Bucket, 18 | ) *CustomerServices { 19 | createCustomerHandler := commands.NewCreateCustomerHandler(es, outbox, manager) 20 | uploadCustomerAvatarHandler := commands.NewCustomerUploadAvatarHandler(es, outbox, manager, bucket) 21 | 22 | customerCommands := commands.NewCustomerCommands(createCustomerHandler, uploadCustomerAvatarHandler) 23 | return &CustomerServices{ 24 | Commands: customerCommands, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /customer/internal/domain/aggregate/command.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/entities" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/events" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/vo" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | func (a *CustomerAggregate) CreateCustomer( 11 | fullName vo.FullName, 12 | addressID uuid.UUID, 13 | email vo.Email, 14 | ) error { 15 | event, err := events.NewCustomerCreatedEvent(a, fullName, addressID, email, vo.NewBalance()) 16 | if err != nil { 17 | return err 18 | } 19 | return a.Apply(event) 20 | } 21 | 22 | func (a *CustomerAggregate) CreateAvatarUri() error { 23 | event, err := events.NewAvatarUriCreatedEvent(a) 24 | if err != nil { 25 | return err 26 | } 27 | return a.Apply(event) 28 | } 29 | 30 | func (a *CustomerAggregate) UpdateTransactionCustomer( 31 | transaction *entities.CustomerTransactions, 32 | ) error { 33 | event, err := events.NewTransactionsUpdatedEvent(a, transaction) 34 | if err != nil { 35 | return err 36 | } 37 | balance := a.GetNewBalance(transaction.TransactionSum, transaction.TransactionType) 38 | eventBalance, errBalance := events.NewBalanceUpdatedEvent(a, balance) 39 | if errBalance != nil { 40 | return errBalance 41 | } 42 | if err := a.Apply(eventBalance); err != nil { 43 | return err 44 | } 45 | return a.Apply(event) 46 | } 47 | -------------------------------------------------------------------------------- /customer/internal/domain/aggregate/store.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/common" 4 | 5 | type EventStore interface { 6 | Create(customer common.Aggregate, tx interface{}) error 7 | Update(customer common.Aggregate, tx interface{}) error 8 | Load(customer common.Aggregate) error 9 | Exists(id string) error 10 | } 11 | -------------------------------------------------------------------------------- /customer/internal/domain/aggregate/utils.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/common" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | func LoadOrderAggregate(_ context.Context, eventStore EventStore, aggregateID uuid.UUID) (*CustomerAggregate, error) { 11 | customer := NewCustomerAggregateWithID(aggregateID) 12 | 13 | err := eventStore.Exists(customer.GetID()) 14 | if err == nil { 15 | return nil, common.ErrAggregateNotFound 16 | } 17 | if err := eventStore.Load(customer); err != nil { 18 | return nil, err 19 | } 20 | fmt.Printf("Aggregate loaded: %v\n", customer) 21 | return customer, nil 22 | } 23 | -------------------------------------------------------------------------------- /customer/internal/domain/common/aggregate.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type AggregateType string 4 | 5 | const ( 6 | aggregateStartVersion = -1 7 | aggregateAppliedEventsInitialCap = 10 8 | aggregateUncommittedEventsInitialCap = 10 9 | ) 10 | 11 | type When interface { 12 | When(event Event) error 13 | } 14 | 15 | // Apply process Aggregate Event 16 | type Apply interface { 17 | Apply(event Event) error 18 | } 19 | 20 | // Load create Aggregate state from Event's. 21 | type Load interface { 22 | Load(events []Event) error 23 | } 24 | 25 | type when func(event Event) error 26 | type Aggregate interface { 27 | When 28 | AggregateRoot 29 | } 30 | type AggregateRoot interface { 31 | GetUncommittedEvents() []Event 32 | GetID() string 33 | SetID(id string) *AggregateBase 34 | GetVersion() int64 35 | SetType(aggregateType AggregateType) 36 | GetType() AggregateType 37 | SetAppliedEvents(events []Event) 38 | GetAppliedEvents() []Event 39 | RaiseEvent(event Event) error 40 | String() string 41 | Load 42 | Apply 43 | } 44 | 45 | type AggregateBase struct { 46 | ID string 47 | Version int64 48 | Type AggregateType 49 | AppliedEvents []Event 50 | UncommittedEvents []Event 51 | when when 52 | } 53 | 54 | // NewAggregateBase create new AggregateBase 55 | // main purpose of this function is to set when function 56 | func NewAggregateBase(when when) *AggregateBase { 57 | if when == nil { 58 | return nil 59 | } 60 | 61 | return &AggregateBase{ 62 | Version: aggregateStartVersion, 63 | AppliedEvents: make([]Event, 0, aggregateAppliedEventsInitialCap), 64 | UncommittedEvents: make([]Event, 0, aggregateUncommittedEventsInitialCap), 65 | when: when, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /customer/internal/domain/common/event.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Event struct { 9 | EventID string 10 | EventType string 11 | Data []byte 12 | Timestamp time.Time 13 | AggregateType AggregateType 14 | AggregateID string 15 | Version int64 16 | Metadata []byte 17 | } 18 | 19 | func NewBaseEvent(aggregate Aggregate, eventType string) Event { 20 | return Event{ 21 | EventID: uuid.New().String(), 22 | AggregateType: aggregate.GetType(), 23 | AggregateID: aggregate.GetID(), 24 | Version: aggregate.GetVersion(), 25 | EventType: eventType, 26 | Timestamp: time.Now().UTC(), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /customer/internal/domain/common/event_methods.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // GetAggregateID is the ID of the Aggregate that the Event belongs to 9 | func (e *Event) GetAggregateID() string { 10 | return e.AggregateID 11 | } 12 | 13 | // GetAggregateType returns the AggregateType of the event. 14 | func (e *Event) GetAggregateType() AggregateType { 15 | return e.AggregateType 16 | } 17 | 18 | // GetEventType returns the EventType of the event. 19 | func (e *Event) GetEventType() string { 20 | return e.EventType 21 | } 22 | 23 | // GetJsonData json unmarshal data attached to the Event. 24 | func (e *Event) GetJsonData(data interface{}) error { 25 | return json.Unmarshal(e.GetData(), data) 26 | } 27 | 28 | // SetJsonData serialize to json and set data attached to the Event. 29 | func (e *Event) SetJsonData(data interface{}) error { 30 | dataBytes, err := json.Marshal(data) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | e.Data = dataBytes 36 | return nil 37 | } 38 | 39 | // GetEventID get EventID of the Event. 40 | func (e *Event) GetEventID() string { 41 | return e.EventID 42 | } 43 | 44 | // GetTimeStamp get timestamp of the Event. 45 | func (e *Event) GetTimeStamp() time.Time { 46 | return e.Timestamp 47 | } 48 | 49 | // GetData The data attached to the Event serialized to bytes. 50 | func (e *Event) GetData() []byte { 51 | return e.Data 52 | } 53 | 54 | // SetAggregateType set the AggregateType that the Event can be applied to. 55 | func (e *Event) SetAggregateType(aggregateType AggregateType) { 56 | e.AggregateType = aggregateType 57 | } 58 | 59 | // SetVersion set the version of the Aggregate. 60 | func (e *Event) SetVersion(aggregateVersion int64) { 61 | e.Version = aggregateVersion 62 | } 63 | -------------------------------------------------------------------------------- /customer/internal/domain/common/exceptions.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrAlreadyExists = errors.New("Already exists") 7 | ErrAggregateNotFound = errors.New("aggregate not found") 8 | ErrInvalidEventType = errors.New("invalid event type") 9 | ErrInvalidCommandType = errors.New("invalid command type") 10 | ErrInvalidAggregate = errors.New("invalid aggregate") 11 | ErrInvalidAggregateID = errors.New("invalid aggregate id") 12 | ErrInvalidEventVersion = errors.New("invalid event version") 13 | ErrNoUncommittedEvents = errors.New("no uncommitted events") 14 | ) 15 | -------------------------------------------------------------------------------- /customer/internal/domain/consts/transaction.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | type TransactionType string 4 | 5 | const ( 6 | DEPOSIT TransactionType = "deposit" 7 | PURCHASE_PENDING TransactionType = "purchase_pending" 8 | PURCHASE_COMPLETED TransactionType = "purchase_completed" 9 | PURCHASE_FAILED TransactionType = "purchase_failed" 10 | ) 11 | -------------------------------------------------------------------------------- /customer/internal/domain/entities/customerTransactions.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/consts" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/vo" 6 | "github.com/google/uuid" 7 | "time" 8 | ) 9 | 10 | type CustomerTransactions struct { 11 | TransactionId uuid.UUID 12 | TransactionType consts.TransactionType 13 | TransactionDate time.Time 14 | TransactionSum vo.Money 15 | Comment string 16 | OrderID *uuid.UUID 17 | } 18 | -------------------------------------------------------------------------------- /customer/internal/domain/exceptions/email.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import "errors" 4 | 5 | var InvalidEmailLength = errors.New("invalid email length") 6 | -------------------------------------------------------------------------------- /customer/internal/domain/exceptions/fullname.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import "errors" 4 | 5 | var InvalidFullName = errors.New("invalid full name") 6 | -------------------------------------------------------------------------------- /customer/internal/domain/exceptions/store.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrCustomerAlreadyExists = errors.New("customer already exists") 7 | ) 8 | -------------------------------------------------------------------------------- /customer/internal/domain/vo/balanceMoney.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | type Money struct { 4 | Value float64 5 | } 6 | 7 | // NewMoney creates new Money value object 8 | func NewMoney() Money { 9 | return Money{Value: 0} 10 | } 11 | 12 | // Sub subtracts money from Money 13 | func (m *Money) Sub(money Money) { 14 | m.Value -= money.Value 15 | } 16 | 17 | // Add adds money to Money 18 | func (m *Money) Add(money Money) { 19 | m.Value += money.Value 20 | } 21 | 22 | // Eq checks if Money is equal to money 23 | func (m *Money) Eq(money Money) bool { 24 | if m.Value == money.Value { 25 | return false 26 | } 27 | return true 28 | } 29 | -------------------------------------------------------------------------------- /customer/internal/domain/vo/customerBalance.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | type Balance struct { 4 | AvailableMoney Money 5 | FrozenMoney Money 6 | } 7 | 8 | // NewBalance creates new Balance value object 9 | func NewBalance() Balance { 10 | return Balance{ 11 | AvailableMoney: NewMoney(), 12 | FrozenMoney: NewMoney(), 13 | } 14 | } 15 | 16 | // SubAvailableMoney Sub returns new Balance value object with subtracted money 17 | func (b *Balance) SubAvailableMoney(money Money) { 18 | b.AvailableMoney.Sub(money) 19 | } 20 | 21 | // AddFrozenMoney Add returns new Balance value object with added money 22 | func (b *Balance) AddFrozenMoney(money Money) { 23 | b.FrozenMoney.Add(money) 24 | } 25 | 26 | // EqAvailableMoney Eq returns true if money is equal to balance 27 | func (b *Balance) EqAvailableMoney(money Money) bool { 28 | if b.AvailableMoney.Eq(money) { 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | // AddAvailableMoney Add returns new Balance value object with added money 35 | func (b *Balance) AddAvailableMoney(money Money) { 36 | b.AvailableMoney.Add(money) 37 | } 38 | func (b *Balance) SubFrozenMoney(money Money) { 39 | b.FrozenMoney.Sub(money) 40 | } 41 | func (b *Balance) AddFrozenMoneyFromAvailable(money Money) { 42 | b.AvailableMoney.Sub(money) 43 | b.FrozenMoney.Add(money) 44 | } 45 | 46 | func (b *Balance) DepositBalance(money Money) Balance { 47 | b.AddAvailableMoney(money) 48 | return *b 49 | } 50 | func (b *Balance) Purchase(money Money) Balance { 51 | b.AddFrozenMoneyFromAvailable(money) 52 | return *b 53 | } 54 | -------------------------------------------------------------------------------- /customer/internal/domain/vo/email.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/exceptions" 5 | "unicode/utf8" 6 | ) 7 | 8 | type Email struct { 9 | Email string `json:"email"` 10 | } 11 | 12 | // NewEmail creates new Email value object 13 | func NewEmail(email string) (Email, error) { 14 | if email == "" { 15 | return Email{}, exceptions.InvalidEmailLength 16 | } 17 | if utf8.RuneCountInString(email) > 255 { 18 | return Email{}, exceptions.InvalidEmailLength 19 | } 20 | return Email{Email: email}, nil 21 | } 22 | -------------------------------------------------------------------------------- /customer/internal/domain/vo/email_test.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestNewValidEmail(t *testing.T) { 10 | email := "some@example.com" 11 | _, err := NewEmail(email) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | } 16 | func TestNewInvalidEmail(t *testing.T) { 17 | email := strings.Repeat("a", 256) 18 | _, err := NewEmail(fmt.Sprintf("some%s@emxample.com", email)) 19 | if err == nil { 20 | t.Error(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /customer/internal/domain/vo/fullName.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/exceptions" 5 | "regexp" 6 | ) 7 | 8 | var NameRegex = regexp.MustCompile(`^[A-Z][a-z]{1,254}$`) 9 | 10 | type FullName struct { 11 | FirstName string 12 | MiddleName string 13 | LastName string 14 | } 15 | 16 | // NewFullName creates new FullName value object 17 | func NewFullName(firstName, middleName, lastName string) (FullName, error) { 18 | if !NameRegex.MatchString(firstName) { 19 | return FullName{}, exceptions.InvalidFullName 20 | } 21 | if !NameRegex.MatchString(middleName) { 22 | return FullName{}, exceptions.InvalidFullName 23 | } 24 | if !NameRegex.MatchString(lastName) { 25 | return FullName{}, exceptions.InvalidFullName 26 | } 27 | 28 | return FullName{ 29 | FirstName: firstName, 30 | MiddleName: middleName, 31 | LastName: lastName, 32 | }, nil 33 | } 34 | 35 | func (f *FullName) String() string { 36 | return f.FirstName + " " + f.MiddleName + " " + f.LastName 37 | } 38 | -------------------------------------------------------------------------------- /customer/internal/domain/vo/fullname_test.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import "testing" 4 | 5 | func TestValidFullName(t *testing.T) { 6 | name := "Mikhail" 7 | middleName := "Vladimirovich" 8 | lastName := "Gulkin" 9 | _, err := NewFullName(name, middleName, lastName) 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | } 14 | func TestInvalidFullName(t *testing.T) { 15 | name := "Smth" 16 | middleName := "" 17 | lastName := "SSS" 18 | _, err := NewFullName(name, middleName, lastName) 19 | if err == nil { 20 | t.Error(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/config.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | Host string 11 | Port int 12 | Database string 13 | User string 14 | Password string 15 | } 16 | 17 | func NewConfig() Config { 18 | port, err := strconv.Atoi(env.GetEnv("POSTGRES_PORT", "5432")) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return Config{ 23 | Host: env.GetEnv("POSTGRES_HOST", "localhost"), 24 | Port: port, 25 | Database: env.GetEnv("POSTGRES_DB", "postgres"), 26 | User: env.GetEnv("POSTGRES_USER", "postgres"), 27 | Password: env.GetEnv("POSTGRES_PASSWORD", "postgres"), 28 | } 29 | } 30 | func (conf *Config) GetDSN() string { 31 | return fmt.Sprintf("postgres://%s:%s@%s:%d/%s", 32 | conf.User, conf.Password, conf.Host, conf.Port, conf.Database, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/convertors.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/common" 5 | ) 6 | 7 | func ConvertAggregateToEntity(customer common.Aggregate) Entity { 8 | return Entity{ 9 | EntityID: customer.GetID(), 10 | EventType: string(customer.GetType()), 11 | EntityVersion: customer.GetVersion(), 12 | } 13 | } 14 | 15 | func ConvertDomainEventToEventModel(event common.Event) Event { 16 | return Event{ 17 | EventID: event.GetEventID(), 18 | EventType: event.GetEventType(), 19 | EventData: event.GetData(), 20 | EntityType: string(event.GetAggregateType()), 21 | EntityID: event.GetAggregateID(), 22 | CreatedAt: event.GetTimeStamp(), 23 | } 24 | } 25 | func ConvertDomainEventToOutboxMessage(event common.Event) OutboxMessage { 26 | return OutboxMessage{ 27 | Exchange: "Customer", 28 | Route: "Customer", 29 | Payload: event.GetData(), 30 | AggregateID: event.GetAggregateID(), 31 | } 32 | } 33 | 34 | func ConvertEventToDomainEvent(event Event) common.Event { 35 | return common.Event{ 36 | EventID: event.EventID, 37 | EventType: event.EventType, 38 | Data: event.EventData, 39 | AggregateType: common.AggregateType(event.EntityType), 40 | AggregateID: event.EntityID, 41 | Timestamp: event.CreatedAt, 42 | } 43 | } 44 | func ConvertEventsToDomainEvents(event []Event) []common.Event { 45 | events := make([]common.Event, 0, len(event)) 46 | for _, e := range event { 47 | events = append(events, ConvertEventToDomainEvent(e)) 48 | } 49 | return events 50 | } 51 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "github.com/jackc/pgx/v4" 6 | ) 7 | 8 | type Connection struct { 9 | *pgx.Conn 10 | } 11 | 12 | func NewConnection(config Config) Connection { 13 | conn, err := pgx.Connect(context.Background(), config.GetDSN()) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return Connection{Conn: conn} 18 | } 19 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/migrations/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS events; 2 | DROP TABLE IF EXISTS entities; -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/migrations/000001_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS events 2 | ( 3 | event_id UUID PRIMARY KEY, 4 | event_type VARCHAR(255) NOT NULL, 5 | event_data JSONB NOT NULL, 6 | entity_type VARCHAR(255) NOT NULL, 7 | entity_id VARCHAR(1000) NOT NULL 8 | ); 9 | CREATE TABLE IF NOT EXISTS entities 10 | ( 11 | entity_type VARCHAR(1000), 12 | entity_id VARCHAR(1000) NOT NULL, 13 | entity_version BIGINT NOT NULL, 14 | PRIMARY KEY (entity_type, entity_id) 15 | ); -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/migrations/000002_creaed_at_field.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE IF EXISTS events 2 | DROP COLUMN IF EXISTS created_at; 3 | 4 | ALTER TABLE IF EXISTS entities 5 | DROP COLUMN IF EXISTS created_at; -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/migrations/000002_creaed_at_field.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE IF EXISTS events 2 | ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; 3 | 4 | ALTER TABLE IF EXISTS entities 5 | ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; 6 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/migrations/000003_outbox.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS outbox; -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/migrations/000003_outbox.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS outbox 2 | ( 3 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 4 | exchange VARCHAR(255) NOT NULL, 5 | route VARCHAR(255) NOT NULL, 6 | payload JSONB NOT NULL, 7 | aggregate_id VARCHAR(255) NOT NULL, 8 | event_status INT NOT NULL DEFAULT 1, 9 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 10 | ); -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/models.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Event struct { 8 | EventID string 9 | EventType string 10 | EventData []byte 11 | EntityType string 12 | EntityID string 13 | CreatedAt time.Time 14 | } 15 | type Entity struct { 16 | EventType string 17 | EntityID string 18 | EntityVersion int64 19 | CreatedAt time.Time 20 | } 21 | type OutboxMessage struct { 22 | Exchange string 23 | Route string 24 | Payload []byte 25 | AggregateID string 26 | EventStatus int 27 | } 28 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/db/uow.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/persistence" 6 | "github.com/jackc/pgx/v4" 7 | ) 8 | 9 | type UoW struct { 10 | Conn Connection 11 | Tx pgx.Tx 12 | } 13 | 14 | // Commit commits the transaction. 15 | func (u *UoW) Commit() error { 16 | return u.Tx.Commit(context.Background()) 17 | } 18 | 19 | // Rollback aborts the transaction. 20 | func (u *UoW) Rollback() error { 21 | err := u.Tx.Rollback(context.Background()) 22 | if err != nil { 23 | return err 24 | } 25 | return nil 26 | } 27 | 28 | // Begin starts a transaction. 29 | func (u *UoW) Begin() (interface{}, error) { 30 | tx, err := u.Conn.Begin(context.Background()) 31 | if err != nil { 32 | return nil, err 33 | } 34 | u.Tx = tx 35 | return u.Tx, nil 36 | } 37 | 38 | type UoWManager struct { 39 | Conn Connection 40 | } 41 | 42 | // GetUoW returns a new UoW. 43 | func (u *UoWManager) GetUoW() persistence.UoW { 44 | return &UoW{ 45 | Conn: u.Conn, 46 | } 47 | } 48 | 49 | func NewUoWManager(conn Connection) *UoWManager { 50 | return &UoWManager{ 51 | Conn: conn, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/di/di.go: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/persistence" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/domain/aggregate" 7 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/infrastructure/db" 8 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/infrastructure/minio" 9 | "go.uber.org/fx" 10 | ) 11 | 12 | var Module = fx.Provide( 13 | fx.Annotate( 14 | db.NewOutbox, 15 | fx.As(new(persistence.Outbox)), 16 | ), 17 | fx.Annotate( 18 | db.NewEventStore, 19 | fx.As(new(aggregate.EventStore)), 20 | ), 21 | fx.Annotate( 22 | minio.NewMinio, 23 | fx.As(new(persistence.Bucket)), 24 | ), 25 | fx.Annotate( 26 | db.NewUoWManager, 27 | fx.As(new(persistence.UoWManager)), 28 | ), 29 | db.NewConfig, 30 | db.NewConnection, 31 | application.NewCustomerServices, 32 | ) 33 | -------------------------------------------------------------------------------- /customer/internal/infrastructure/minio/minio.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env" 7 | "github.com/minio/minio-go/v7" 8 | "github.com/minio/minio-go/v7/pkg/credentials" 9 | "log" 10 | ) 11 | 12 | type Client struct { 13 | *minio.Client 14 | } 15 | 16 | func NewMinio() *Client { 17 | minioClient, err := minio.New( 18 | env.GetEnv("MINIO_HOST", "localhost:9000"), 19 | &minio.Options{ 20 | Creds: credentials.NewStaticV4( 21 | env.GetEnv("MINIO_ACCESS", "g7D6LHivwHFXEZgo7nlv"), 22 | env.GetEnv("MINIO_SECRET", "XVNhSAYuv4ossGre91ZugQlRrjhHt6fdnesSnipx"), 23 | "", 24 | ), 25 | Secure: false, 26 | }) 27 | if err != nil { 28 | panic(err) 29 | } 30 | client := Client{Client: minioClient} 31 | SetupBuckets(&client) 32 | return &client 33 | 34 | } 35 | func SetupBuckets(client *Client) { 36 | err := client.MakeBucket(context.Background(), "customer-avatars", minio.MakeBucketOptions{}) 37 | if err != nil { 38 | exists, errBucketExists := client.BucketExists(context.Background(), "customer-avatars") 39 | if errBucketExists == nil && exists { 40 | log.Printf("We already own %s\n", "customer-avatars") 41 | } else { 42 | log.Fatalln(err) 43 | } 44 | } else { 45 | log.Printf("Successfully created %s\n", "customer-avatars") 46 | } 47 | } 48 | 49 | // UploadAvatar uploads avatar to minio 50 | func (client *Client) UploadAvatar(ctx context.Context, name string, fileBuffer []byte) error { 51 | // Upload the photo file with PutObject 52 | reader := bytes.NewReader(fileBuffer) 53 | 54 | _, err := client.PutObject(ctx, "customer-avatars", name, reader, reader.Size(), minio.PutObjectOptions{ 55 | ContentType: "image/jpeg", 56 | }) 57 | if err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go.uber.org/fx" 7 | ) 8 | 9 | var Module = fx.Options( 10 | fx.Provide( 11 | NewEngine, 12 | NewCustomerHandler, 13 | NewMiddlewares, 14 | NewFiberGroup, 15 | fx.Annotate( 16 | NewCustomerRouter, 17 | fx.As(new(Route)), 18 | ), 19 | ), 20 | fx.Invoke(Start), 21 | ) 22 | 23 | func Start( 24 | lifecycle fx.Lifecycle, 25 | route Route, 26 | engine Engine, 27 | ) { 28 | engine.Setup() 29 | route.Setup() 30 | 31 | lifecycle.Append( 32 | fx.Hook{ 33 | OnStart: func(context.Context) error { 34 | go func() { 35 | defer func() { 36 | if r := recover(); r != nil { 37 | fmt.Printf("Recovered when boot grpc server, r %s", r) 38 | } 39 | }() 40 | err := engine.Run() 41 | if err != nil { 42 | panic(err) 43 | } 44 | }() 45 | return nil 46 | }, 47 | OnStop: func(context.Context) error { 48 | return engine.Shutdown() 49 | }, 50 | }, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /api/v1 2 | definitions: 3 | api.Error: 4 | properties: 5 | error: 6 | type: string 7 | type: object 8 | commands.UploadAvatarDTO: 9 | properties: 10 | avatarUri: 11 | type: string 12 | customerID: 13 | type: string 14 | eventID: 15 | type: string 16 | required: 17 | - avatarUri 18 | - customerID 19 | - eventID 20 | type: object 21 | host: localhost:8000 22 | info: 23 | contact: {} 24 | description: This is a sample server for Customer API. 25 | title: Customer API 26 | version: "1.0" 27 | paths: 28 | /upload-avatar/{id}/: 29 | post: 30 | consumes: 31 | - multipart/form-data 32 | description: Upload new avatar for customer 33 | parameters: 34 | - description: Customer ID 35 | in: path 36 | name: id 37 | required: true 38 | type: string 39 | - description: Avatar 40 | in: formData 41 | name: file 42 | required: true 43 | type: file 44 | produces: 45 | - application/json 46 | responses: 47 | "200": 48 | description: OK 49 | schema: 50 | $ref: '#/definitions/commands.UploadAvatarDTO' 51 | "400": 52 | description: Bad Request 53 | schema: 54 | $ref: '#/definitions/api.Error' 55 | summary: Upload new avatar for customer 56 | tags: 57 | - Customer 58 | swagger: "2.0" 59 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/dto.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Error struct { 4 | Error string `json:"error"` 5 | } 6 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/engine.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env" 5 | "github.com/gofiber/fiber/v2" 6 | "github.com/gofiber/swagger" 7 | ) 8 | 9 | type Engine struct { 10 | app *fiber.App 11 | } 12 | 13 | func NewEngine() Engine { 14 | return Engine{app: fiber.New()} 15 | } 16 | func (e *Engine) Run() error { 17 | return e.app.Listen(env.GetEnv("API_PORT", ":8000")) 18 | } 19 | func (e *Engine) Setup() { 20 | e.app.Get("/swagger/*", swagger.HandlerDefault) 21 | } 22 | func (e *Engine) Shutdown() error { 23 | return e.app.Shutdown() 24 | } 25 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/group.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/gofiber/fiber/v2" 4 | 5 | type FiberGroup struct { 6 | fiber.Router 7 | } 8 | 9 | func NewFiberGroup(engine Engine, middlewares Middlewares) FiberGroup { 10 | return FiberGroup{Router: engine.app.Group("/api/v1", middlewares...)} 11 | } 12 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/handlers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/commands" 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | func NewCustomerHandler(service *application.CustomerServices) *Handler { 11 | return &Handler{ 12 | cs: service, 13 | } 14 | } 15 | 16 | type Handler struct { 17 | cs *application.CustomerServices 18 | } 19 | 20 | // UploadNewAvatar godoc 21 | // @Summary Upload new avatar for customer 22 | // @Description Upload new avatar for customer 23 | // @Tags Customer 24 | // @Accept multipart/form-data 25 | // @Produce json 26 | // @Param id path string true "Customer ID" 27 | // @Param file formData file true "Avatar" 28 | // @Success 200 {object} commands.UploadAvatarDTO 29 | // @Failure 400 {object} Error 30 | // @Router /upload-avatar/{id}/ [post] 31 | func (h *Handler) UploadNewAvatar(context *fiber.Ctx) error { 32 | id := context.Params("id") 33 | uid, err := uuid.Parse(id) 34 | if err != nil { 35 | return context.Status(fiber.StatusBadRequest).JSON(fiber.Map{ 36 | "error": err.Error(), 37 | }) 38 | } 39 | if len(context.Body()) == 0 { 40 | return context.Status(fiber.StatusBadRequest).JSON( 41 | &Error{Error: "Body can't be empty"}, 42 | ) 43 | } 44 | data, err := h.cs.Commands.UploadCustomerAvatar.Handle(commands.UploadAvatarCommand{ 45 | CustomerID: uid, 46 | Avatar: context.Body(), 47 | }) 48 | if err != nil { 49 | return context.Status(fiber.StatusBadRequest).JSON( 50 | &Error{Error: err.Error()}, 51 | ) 52 | } 53 | return context.Status(fiber.StatusOK).JSON(data) 54 | } 55 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/main.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Start starts the server. 4 | // @title Customer API 5 | // @version 1.0 6 | // @description This is a sample server for Customer API. 7 | // @host localhost:8000 8 | // @BasePath /api/v1 9 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/middlewares.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/gofiber/fiber/v2" 4 | 5 | type Middlewares []fiber.Handler 6 | 7 | func NewMiddlewares() Middlewares { 8 | return Middlewares{} 9 | } 10 | -------------------------------------------------------------------------------- /customer/internal/presentation/api/routes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/presentation/api/docs" 5 | "github.com/gofiber/fiber/v2" 6 | ) 7 | 8 | type Routes struct { 9 | FiberGroup 10 | controller *Handler 11 | } 12 | 13 | type Route interface { 14 | Setup() 15 | } 16 | 17 | func (r Routes) Setup() { 18 | r.Add(fiber.MethodPost, "/upload-avatar/:id", r.controller.UploadNewAvatar) 19 | } 20 | 21 | func NewCustomerRouter( 22 | group FiberGroup, 23 | controller *Handler, 24 | ) *Routes { 25 | return &Routes{controller: controller, FiberGroup: group} 26 | } 27 | -------------------------------------------------------------------------------- /customer/internal/presentation/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env" 5 | "github.com/goccy/go-graphviz" 6 | "go.uber.org/fx" 7 | "os" 8 | ) 9 | 10 | var Module = fx.Options(fx.Invoke(Start)) 11 | 12 | func Start( 13 | diGraph fx.DotGraph, 14 | ) { 15 | if env.GetEnv("APP_MODE", "development") != "development" { 16 | return 17 | } 18 | g := graphviz.New() 19 | graph, _ := graphviz.ParseBytes([]byte(diGraph)) 20 | os.WriteFile("./graph.gv", []byte(diGraph), 0644) 21 | g.RenderFilename(graph, graphviz.PNG, "./graph.png") 22 | g.RenderFilename(graph, graphviz.SVG, "./graph.svg") 23 | } 24 | -------------------------------------------------------------------------------- /customer/internal/presentation/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env" 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Options( 11 | fx.Provide( 12 | NewGrpcServer, 13 | NewCustomerGrpcService, 14 | ), 15 | fx.Invoke(Start), 16 | ) 17 | 18 | func Start(lifecycle fx.Lifecycle, server *Server) { 19 | lifecycle.Append( 20 | fx.Hook{ 21 | OnStart: func(context.Context) error { 22 | go func() { 23 | defer func() { 24 | if r := recover(); r != nil { 25 | fmt.Printf("Recovered when boot grpc server, r %s", r) 26 | } 27 | }() 28 | err := server.Run(env.GetEnv("GRPC_PORT", "50052")) 29 | if err != nil { 30 | panic(err) 31 | } 32 | }() 33 | return nil 34 | }, 35 | OnStop: func(context.Context) error { 36 | server.Down() 37 | return nil 38 | }, 39 | }, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /customer/internal/presentation/grpc/server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/presentation/grpc/pb" 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/keepalive" 8 | "google.golang.org/grpc/reflection" 9 | "log" 10 | "net" 11 | "time" 12 | ) 13 | 14 | const ( 15 | maxConnectionIdle = 5 16 | gRPCTimeout = 15 17 | maxConnectionAge = 5 18 | gRPCTime = 10 19 | ) 20 | 21 | type Server struct { 22 | service pb.CustomerServiceServer 23 | server *grpc.Server 24 | } 25 | 26 | func NewGrpcServer(service pb.CustomerServiceServer) *Server { 27 | return &Server{ 28 | service: service, 29 | } 30 | } 31 | 32 | func (g *Server) Run(port string) error { 33 | s := grpc.NewServer(grpc.KeepaliveParams(keepalive.ServerParameters{ 34 | MaxConnectionIdle: maxConnectionIdle * time.Minute, 35 | Timeout: gRPCTimeout * time.Second, 36 | MaxConnectionAge: maxConnectionAge * time.Minute, 37 | Time: gRPCTime * time.Minute, 38 | })) 39 | pb.RegisterCustomerServiceServer(s, g.service) 40 | lis, err := net.Listen("tcp", fmt.Sprintf(":%s", port)) 41 | 42 | if err != nil { 43 | log.Fatalf("Failed to listen: %v", err) 44 | } 45 | reflection.Register(s) 46 | g.server = s 47 | err = s.Serve(lis) 48 | if err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | func (g *Server) Down() { 54 | g.server.GracefulStop() 55 | } 56 | -------------------------------------------------------------------------------- /customer/internal/presentation/grpc/service.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application" 6 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/application/commands" 7 | "github.com/MikhailGulkin/simpleGoOrderApp/customer/internal/presentation/grpc/pb" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type CustomerGrpcService struct { 12 | cs *application.CustomerServices 13 | } 14 | 15 | func NewCustomerGrpcService(cs *application.CustomerServices) pb.CustomerServiceServer { 16 | return &CustomerGrpcService{ 17 | cs: cs, 18 | } 19 | } 20 | func (s *CustomerGrpcService) CreateCustomer( 21 | _ context.Context, request *pb.CreateCustomerRequest, 22 | ) (*pb.CreateCustomerResponse, error) { 23 | customerID, customerErr := uuid.Parse(request.CustomerID) 24 | if customerErr != nil { 25 | return nil, customerErr 26 | } 27 | addressID, addressErr := uuid.Parse(request.AddressID) 28 | if addressErr != nil { 29 | return nil, addressErr 30 | } 31 | c := commands.CreateCustomerCommand{ 32 | CustomerID: customerID, 33 | FirstName: request.FirstName, 34 | LastName: request.LastName, 35 | MiddleName: request.MiddleName, 36 | AddressID: addressID, 37 | Email: request.Email, 38 | } 39 | 40 | response, err := s.cs.Commands.CreateCustomer.Handle(c) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &pb.CreateCustomerResponse{ 45 | Id: response.CustomerID, 46 | EventID: response.EventID, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /customer/protobuf/customer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./servicespb"; 4 | 5 | message CreateCustomerRequest { 6 | string customerID = 1; 7 | string firstName = 2; 8 | string middleName = 3; 9 | string lastName = 4; 10 | string email = 5; 11 | string addressID = 6; 12 | } 13 | 14 | message CreateCustomerResponse { 15 | string id = 1; 16 | string eventID = 2; 17 | } 18 | 19 | service CustomerService { 20 | rpc CreateCustomer(CreateCustomerRequest) returns (CreateCustomerResponse); 21 | } -------------------------------------------------------------------------------- /order/.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=OrderApp 2 | POSTGRES_USER=postgres 3 | POSTGRES_PASSWORD=1234 4 | POSTGRES_HOST=localhost 5 | POSTGRES_PORT=5432 6 | 7 | 8 | RABBITMQ_DEFAULT_USER='admin' 9 | RABBITMQ_DEFAULT_PASS='admin' 10 | 11 | GF_SECURITY_ADMIN_USER='admin' 12 | GF_SECURITY_ADMIN_PASSWORD='admin' -------------------------------------------------------------------------------- /order/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20.5 AS build 2 | WORKDIR /app 3 | COPY /order /app 4 | COPY order/go.mod order/go.sum /modules/ 5 | 6 | RUN go mod download 7 | RUN go env -w CGO_ENABLED=0 8 | 9 | RUN go build -o main ./cmd/main.go 10 | 11 | FROM ubuntu:20.04 12 | RUN apt-get update && apt-get install -y curl 13 | COPY --from=build /app/configs/app/prod.toml /app/main /usr/local/bin/ -------------------------------------------------------------------------------- /order/Makefile: -------------------------------------------------------------------------------- 1 | up-containers: 2 | docker-compose -p order-app -f ./docker-compose.dev.yaml up --build 3 | up-container-tests: 4 | docker-compose -p test-container-app -f ./docker-compose.container.test.yaml up --build 5 | up-tests: 6 | docker-compose -p test-app -f ./docker-compose.test.yaml up --build 7 | -------------------------------------------------------------------------------- /order/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/consumer" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/config" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/graph" 11 | "go.uber.org/fx" 12 | "go.uber.org/fx/fxevent" 13 | ) 14 | 15 | func main() { 16 | fx.New( 17 | fx.WithLogger(func(logger logger.Logger) fxevent.Logger { 18 | return logger.GetFxLogger() 19 | }), 20 | di.Module, 21 | config.Module, 22 | api.Module, 23 | consumer.Module, 24 | cron.Module, 25 | graph.Module, 26 | ).Run() 27 | } 28 | -------------------------------------------------------------------------------- /order/configs/app.example/prod.example.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | mode='production' 3 | 4 | [api] 5 | host = "0.0.0.0" 6 | port = 8000 7 | base_url_prefix = "/api/v1" 8 | 9 | [db] 10 | host = "orderService.postgres" 11 | port = 5432 12 | database = "OrderApp" 13 | user = "postgres" 14 | password = "1234" 15 | migration = true 16 | logging = true 17 | max_idle_connection = 100 18 | 19 | [broker] 20 | host = "orderService.rabbitmq" 21 | port = 5672 22 | login = "admin" 23 | password = "admin" 24 | max_channels = 10 25 | 26 | [cron] 27 | seconds = 5 28 | 29 | [logging] 30 | log_output='' 31 | log_level='info' 32 | 33 | [cache] 34 | host="orderService.redis" 35 | port=6379 36 | password="" 37 | db=0 -------------------------------------------------------------------------------- /order/configs/app/dev.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | mode='development' 3 | 4 | [api] 5 | host = "localhost" 6 | port = 8000 7 | base_url_prefix = "/api/v1" 8 | 9 | [db] 10 | host = "localhost" 11 | port = 5431 12 | database = "OrderApp" 13 | user = "postgres" 14 | password = "1234" 15 | migration = true 16 | logging = true 17 | max_idle_connection = 100 18 | 19 | [broker] 20 | host = "localhost" 21 | port = 5672 22 | login = "admin" 23 | password = "admin" 24 | max_channels = 10 25 | 26 | [cron] 27 | seconds = 5 28 | 29 | [logging] 30 | log_output='' 31 | log_level="info" 32 | 33 | [cache] 34 | host="localhost" 35 | port=6379 36 | password="" 37 | db=0 -------------------------------------------------------------------------------- /order/configs/app/prod.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | mode='production' 3 | 4 | [api] 5 | host = "0.0.0.0" 6 | port = 8000 7 | base_url_prefix = "/api/v1" 8 | 9 | [db] 10 | host = "orderService.postgres" 11 | port = 5432 12 | database = "OrderApp" 13 | user = "postgres" 14 | password = "1234" 15 | migration = true 16 | logging = true 17 | max_idle_connection = 100 18 | 19 | [broker] 20 | host = "orderService.rabbitmq" 21 | port = 5672 22 | login = "admin" 23 | password = "admin" 24 | max_channels = 10 25 | 26 | [cron] 27 | seconds = 5 28 | 29 | [logging] 30 | log_output='' 31 | log_level='info' 32 | 33 | [cache] 34 | host="orderService.redis" 35 | port=6379 36 | password="" 37 | db=0 -------------------------------------------------------------------------------- /order/configs/db/init.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | -------------------------------------------------------------------------------- /order/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikhailGulkin/CleanGolangOrderApp/605b38bc0e6d3d2df01b0421311773f3a60c0073/order/graph.png -------------------------------------------------------------------------------- /order/internal/application/common/consts/outbox/outbox.go: -------------------------------------------------------------------------------- 1 | package outbox 2 | 3 | type EventStatus uint 4 | 5 | const ( 6 | Undefined EventStatus = iota 7 | Awaiting = 1 8 | Processed = 2 9 | Sagas = 3 10 | Rejected = 4 11 | ) 12 | -------------------------------------------------------------------------------- /order/internal/application/common/consts/saga.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | type SagaStatus string 4 | 5 | const ( 6 | Pending SagaStatus = "Pending" 7 | Rejected SagaStatus = "Rejected" 8 | Approved SagaStatus = "Approved" 9 | ) 10 | -------------------------------------------------------------------------------- /order/internal/application/common/dto/sequence.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import f "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 4 | 5 | type BaseSequence struct { 6 | Count uint `json:"count"` 7 | Limit uint `json:"limit,omitempty"` 8 | Offset uint `json:"offset,omitempty"` 9 | Order f.BaseOrder `json:"order,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/application/common/interfaces/broker/broker.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import "context" 4 | 5 | type MessageBroker interface { 6 | PublishMessage(ctx context.Context, exchangeName, routingKey string, message []byte) error 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/application/common/interfaces/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | type Logger interface { 4 | Info(args ...interface{}) 5 | } 6 | -------------------------------------------------------------------------------- /order/internal/application/common/interfaces/persistence/filters/filters.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type BaseOrder string 4 | 5 | const ( 6 | ASC BaseOrder = "asc" 7 | DESC BaseOrder = "desc" 8 | ) 9 | 10 | type BaseFilters struct { 11 | Limit uint 12 | Offset uint 13 | Order BaseOrder 14 | } 15 | 16 | func (filter BaseFilters) Create(limit uint, offset uint, order BaseOrder) BaseFilters { 17 | return BaseFilters{Limit: limit, Offset: offset, Order: order} 18 | } 19 | -------------------------------------------------------------------------------- /order/internal/application/common/interfaces/persistence/query/query.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 4 | 5 | type BaseListQueryParams struct { 6 | Offset uint 7 | Limit uint 8 | Order filters.BaseOrder 9 | } 10 | -------------------------------------------------------------------------------- /order/internal/application/common/interfaces/persistence/repo/outbox.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 4 | 5 | type OutboxRepo interface { 6 | AddEvents(events []events.Event, tx interface{}) error 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/application/common/interfaces/persistence/uow.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | type UoW interface { 4 | Get() UoW 5 | StartTx() any 6 | GetTx() any 7 | Commit() error 8 | Rollback() 9 | } 10 | -------------------------------------------------------------------------------- /order/internal/application/order/command/delete_order.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/logger" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence" 7 | outboxRepo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/repo" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/command" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/repo" 10 | ) 11 | 12 | type DeleteOrderImpl struct { 13 | command.DeleteOrder 14 | repo.OrderRepo 15 | persistence.UoW 16 | outboxRepo.OutboxRepo 17 | logger.Logger 18 | } 19 | 20 | func (interactor *DeleteOrderImpl) Delete(c command.DeleteOrderCommand) error { 21 | order, err := interactor.OrderRepo.AcquiredOrder(c.OrderID) 22 | if err != nil { 23 | return err 24 | } 25 | err = order.DeleteOrder() 26 | if err != nil { 27 | return err 28 | } 29 | err = interactor.OrderRepo.UpdateOrder(&order, interactor.UoW.StartTx()) 30 | if err != nil { 31 | interactor.UoW.Rollback() 32 | return err 33 | } 34 | err = interactor.OutboxRepo.AddEvents(order.PullEvents(), interactor.UoW.GetTx()) 35 | if err != nil { 36 | return err 37 | } 38 | err = interactor.UoW.Commit() 39 | if err != nil { 40 | return err 41 | } 42 | interactor.Logger.Info(fmt.Sprintf("Delete Event, id %s", c.OrderID.String())) 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /order/internal/application/order/dto/order.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/dto" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | type Order struct { 10 | OrderStatus string `json:"orderStatus"` 11 | PaymentMethod string `json:"paymentMethod"` 12 | ClientID uuid.UUID `json:"clientID"` 13 | AddressID uuid.UUID `json:"addressID"` 14 | OrderID uuid.UUID `json:"OrderID"` 15 | SerialNumber int `json:"serialNumber"` 16 | Products []Product `json:"products"` 17 | CreatedAt time.Time `json:"createdAt"` 18 | TotalPrice float64 `json:"totalPrice"` 19 | } 20 | type Orders struct { 21 | Orders []Order `json:"orders"` 22 | dto.BaseSequence 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/application/order/dto/products.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/google/uuid" 4 | 5 | type Product struct { 6 | ProductID uuid.UUID `json:"productID"` 7 | Name string `json:"name"` 8 | ActualPrice float64 `json:"actualPrice"` 9 | } 10 | -------------------------------------------------------------------------------- /order/internal/application/order/exceptions/order.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common" 6 | ) 7 | 8 | type OrderIDNotExist struct { 9 | common.CustomException 10 | } 11 | 12 | func (e OrderIDNotExist) Exception(context string) OrderIDNotExist { 13 | return OrderIDNotExist{ 14 | CustomException: common.CustomException{ 15 | Message: "Order with this id not exist;", 16 | Ctx: fmt.Sprintf("id `%s`", context), 17 | }} 18 | } 19 | 20 | type ProductIDsNotExist struct { 21 | common.CustomException 22 | } 23 | 24 | func (e ProductIDsNotExist) Exception(context []string) ProductIDsNotExist { 25 | return ProductIDsNotExist{ 26 | CustomException: common.CustomException{ 27 | Message: "Product with this ids not exist;", 28 | Ctx: fmt.Sprintf("ids: `%v`", context), 29 | }} 30 | } 31 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/cache/order_create.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type EventType struct { 9 | EventType string `json:"eventType"` 10 | } 11 | type Product struct { 12 | ProductID uuid.UUID `json:"productID"` 13 | Name string `json:"name"` 14 | ActualPrice float64 `json:"totalPrice"` 15 | } 16 | type OrderCreateSubscribe struct { 17 | OrderID uuid.UUID `json:"orderID"` 18 | ClientID uuid.UUID `json:"clientID"` 19 | PaymentMethod string `json:"paymentMethod"` 20 | OrderStatus string `json:"orderStatus"` 21 | DeliveryAddressID uuid.UUID `json:"deliveryAddressID"` 22 | Products []Product `json:"products"` 23 | SerialNumber int `json:"serialNumber"` 24 | TotalPrice float64 `json:"totalPrice"` 25 | CreatedAt time.Time `json:"eventTimeStamp"` 26 | } 27 | type OrderAddProductSubscribe struct { 28 | OrderID uuid.UUID `json:"orderID"` 29 | ClientID uuid.UUID `json:"clientID"` 30 | ProductName string `json:"productName"` 31 | ProductID uuid.UUID `json:"productID"` 32 | ProductPrice float64 `json:"productPrice"` 33 | } 34 | type ProductEvent struct { 35 | ProductID uuid.UUID `json:"productID"` 36 | Name string `json:"name"` 37 | ActualPrice float64 `json:"actualPrice"` 38 | } 39 | type OrderDeleteEvent struct { 40 | OrderID uuid.UUID `json:"orderID"` 41 | } 42 | type OrderCache interface { 43 | OrderCreateEvent(event OrderCreateSubscribe) 44 | OrderAddProductEvent(event OrderAddProductSubscribe) 45 | OrderDeleteEvent(event OrderDeleteEvent) 46 | } 47 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/command/create_order.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type CreateOrderCommand struct { 8 | OrderID uuid.UUID `json:"orderID" binding:"required"` 9 | PaymentMethod string `json:"paymentMethod" binding:"required"` 10 | ProductsIDs []uuid.UUID `json:"productsIDs" binding:"required"` 11 | UserID uuid.UUID `json:"userID" binding:"required"` 12 | DeliveryAddress uuid.UUID `json:"deliveryAddress" binding:"required"` 13 | } 14 | 15 | type CreateOrder interface { 16 | Create(command CreateOrderCommand) error 17 | } 18 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/command/delete_order.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "github.com/google/uuid" 4 | 5 | type DeleteOrderCommand struct { 6 | OrderID uuid.UUID `json:"orderID,omitempty"` 7 | } 8 | type DeleteOrder interface { 9 | Delete(command DeleteOrderCommand) error 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/persistence/dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 5 | order "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/entities" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type OrderDAO interface { 10 | GetProductsByIDs([]uuid.UUID) ([]order.OrderProduct, error) 11 | GetClientByID(uuid.UUID) (order.OrderClient, error) 12 | GetAddressByID(uuid.UUID) (order.OrderAddress, error) 13 | } 14 | type OrderSagaDAO interface { 15 | CheckSagaCompletion(uuid.UUID) (bool, error) 16 | UpdateOrderSagaStatus(uuid.UUID, string, interface{}) error 17 | DeleteOrder(uuid.UUID, interface{}) error 18 | } 19 | type OrderCacheDAO interface { 20 | GetOrder(uuid.UUID) dto.Order 21 | SaveOrder(dto.Order) error 22 | DeleteOrder(uuid.UUID) error 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/persistence/filters/filters.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 4 | 5 | type GetAllOrdersFilters struct { 6 | filters.BaseFilters 7 | } 8 | type GetAllOrdersByUserIDFilters struct { 9 | filters.BaseFilters 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/persistence/reader/order.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/filters" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type OrderCacheReader interface { 10 | GetAllOrders(filters filters.GetAllOrdersFilters) ([]dto.Order, error) 11 | GetAllOrdersByUserID(userID uuid.UUID, filters filters.GetAllOrdersByUserIDFilters) ([]dto.Order, error) 12 | GetOrderByID(uuid.UUID) (dto.Order, error) 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/persistence/repo/order.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | order "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/aggregate" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type OrderRepo interface { 9 | AcquireLastOrder() (order.Order, error) 10 | AddOrder(order *order.Order, tx interface{}) error 11 | AcquiredOrder(uuid uuid.UUID) (order.Order, error) 12 | UpdateOrder(order *order.Order, tx interface{}) error 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/query/get_all_orders.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/query" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 6 | ) 7 | 8 | type GetAllOrderQuery struct { 9 | query.BaseListQueryParams 10 | } 11 | 12 | type GetAllOrders interface { 13 | Get(query GetAllOrderQuery) (dto.Orders, error) 14 | } 15 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/query/get_all_orders_by_user.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/query" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type GetAllOrderByUserIDQuery struct { 10 | UserID uuid.UUID `json:"userID"` 11 | query.BaseListQueryParams 12 | } 13 | 14 | type GetAllOrdersByUserID interface { 15 | Get(query GetAllOrderByUserIDQuery) (dto.Orders, error) 16 | } 17 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/query/get_order_by_id.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type GetOrderByIDQuery struct { 9 | ID uuid.UUID `json:"ID"` 10 | } 11 | type GetOrderByID interface { 12 | Get(query GetOrderByIDQuery) (dto.Order, error) 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/application/order/interfaces/saga/saga.go: -------------------------------------------------------------------------------- 1 | package saga 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/consts" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type Message struct { 9 | OrderID uuid.UUID `json:"orderID"` 10 | OrderType consts.SagaStatus `json:"orderType"` 11 | } 12 | 13 | type CreateOrder interface { 14 | CheckStatus(message Message) 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/application/order/query/get_all_orders.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/dto" 5 | f "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/filters" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/reader" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/query" 10 | ) 11 | 12 | type GetAllOrderImpl struct { 13 | query.GetAllOrders 14 | reader.OrderCacheReader 15 | } 16 | 17 | func (interactor *GetAllOrderImpl) Get(q query.GetAllOrderQuery) (dto.Orders, error) { 18 | orders, err := interactor.OrderCacheReader.GetAllOrders( 19 | filters.GetAllOrdersFilters{BaseFilters: f.BaseFilters{ 20 | Limit: q.Limit, Offset: q.Offset, Order: q.Order, 21 | }}, 22 | ) 23 | if err != nil { 24 | return dto.Orders{}, err 25 | } 26 | return dto.Orders{Orders: orders, BaseSequence: base.BaseSequence{ 27 | Count: uint(len(orders)), 28 | Limit: q.Limit, 29 | Offset: q.Offset, 30 | Order: q.Order, 31 | }}, nil 32 | } 33 | -------------------------------------------------------------------------------- /order/internal/application/order/query/get_all_orders_by_user.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/dto" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/logger" 7 | baseFilters "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/filters" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/reader" 11 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/query" 12 | ) 13 | 14 | type GetAllOrdersByUserIDImpl struct { 15 | query.GetAllOrdersByUserID 16 | reader.OrderCacheReader 17 | logger.Logger 18 | } 19 | 20 | func (interactor *GetAllOrdersByUserIDImpl) Get(q query.GetAllOrderByUserIDQuery) (dto.Orders, error) { 21 | orders, err := interactor.OrderCacheReader.GetAllOrdersByUserID( 22 | q.UserID, 23 | filters.GetAllOrdersByUserIDFilters{BaseFilters: baseFilters.BaseFilters{Limit: q.Limit, Offset: q.Offset, Order: q.Order}}, 24 | ) 25 | l := uint(len(orders)) 26 | if err != nil { 27 | return dto.Orders{}, err 28 | } 29 | interactor.Logger.Info( 30 | fmt.Sprintf("Get all Orders by user id %s, count: %d, order: %s, limit: %d, offset: %d", 31 | q.UserID.String(), l, q.Order, q.Limit, q.Offset, 32 | )) 33 | return dto.Orders{Orders: orders, 34 | BaseSequence: base.BaseSequence{Count: uint(len(orders)), Limit: q.Limit, Offset: q.Offset, Order: q.Order}, 35 | }, err 36 | } 37 | -------------------------------------------------------------------------------- /order/internal/application/order/query/get_order_by_id.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/reader" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/query" 7 | ) 8 | 9 | type GetOrderByIDImpl struct { 10 | reader.OrderCacheReader 11 | query.GetOrderByID 12 | } 13 | 14 | func (interactor *GetOrderByIDImpl) Get(query query.GetOrderByIDQuery) (dto.Order, error) { 15 | order, err := interactor.OrderCacheReader.GetOrderByID(query.ID) 16 | if err != nil { 17 | return dto.Order{}, err 18 | } 19 | return order, nil 20 | } 21 | -------------------------------------------------------------------------------- /order/internal/application/product/command/create_product.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/command" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/dao" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/entities" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/vo" 9 | ) 10 | 11 | type CreateProductImpl struct { 12 | dao.ProductDAO 13 | persistence.UoW 14 | command.CreateProduct 15 | } 16 | 17 | func (interactor *CreateProductImpl) Create(command command.CreateProductCommand) error { 18 | price, priceErr := vo.ProductPrice{}.Create(command.Price) 19 | if priceErr != nil { 20 | return priceErr 21 | } 22 | discount, discountErr := vo.ProductDiscount{}.Create(command.Discount) 23 | if discountErr != nil { 24 | return discountErr 25 | } 26 | 27 | productEntity, err := entities.Product{}.Create( 28 | vo.ProductID{Value: command.ProductID}, 29 | price, 30 | discount, 31 | command.Description, 32 | command.Name, 33 | ) 34 | if err != nil { 35 | return err 36 | } 37 | err = interactor.ProductDAO.CreateProduct(productEntity, interactor.UoW.StartTx()) 38 | if err != nil { 39 | return err 40 | } 41 | err = interactor.UoW.Commit() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /order/internal/application/product/command/update_product_name.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/command" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/dao" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/vo" 8 | ) 9 | 10 | type UpdateProductNameImpl struct { 11 | dao.ProductDAO 12 | persistence.UoW 13 | command.UpdateProductName 14 | } 15 | 16 | func (interactor *UpdateProductNameImpl) Update(command command.UpdateProductNameCommand) error { 17 | productID := vo.ProductID{Value: command.ProductID} 18 | 19 | productEntity, err := interactor.ProductDAO.GetProductByID(productID) 20 | if err != nil { 21 | return err 22 | } 23 | err = productEntity.UpdateName(command.ProductName) 24 | if err != nil { 25 | return err 26 | } 27 | err = interactor.ProductDAO.UpdateProduct(productEntity, interactor.UoW.StartTx()) 28 | if err != nil { 29 | return err 30 | } 31 | err = interactor.UoW.Commit() 32 | if err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /order/internal/application/product/dto/product.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/dto" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | type Product struct { 10 | ID uuid.UUID `json:"id"` 11 | Name string `json:"name"` 12 | Price float64 `json:"price"` 13 | Discount int32 `json:"discount"` 14 | Quantity int32 `json:"quantity"` 15 | Description string `json:"description"` 16 | Availability bool `json:"availability"` 17 | CreatedAt time.Time `json:"created_at"` 18 | } 19 | 20 | type Products struct { 21 | Products []Product `json:"products,omitempty"` 22 | dto.BaseSequence 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/application/product/exceptions/product.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common" 6 | ) 7 | 8 | type ProductNameNotExist struct { 9 | common.CustomException 10 | } 11 | 12 | func (e ProductNameNotExist) Exception(context string) ProductNameNotExist { 13 | return ProductNameNotExist{ 14 | CustomException: common.CustomException{ 15 | Message: "Product with this name not exist;", 16 | Ctx: fmt.Sprintf("name `%s`", context), 17 | }} 18 | } 19 | 20 | type ProductIDNotExist struct { 21 | common.CustomException 22 | } 23 | 24 | func (e ProductIDNotExist) Exception(context string) ProductIDNotExist { 25 | return ProductIDNotExist{ 26 | CustomException: common.CustomException{ 27 | Message: "Product with this id not exist;", 28 | Ctx: fmt.Sprintf("id `%s`", context), 29 | }} 30 | } 31 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/command/create_product.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "github.com/google/uuid" 4 | 5 | type CreateProductCommand struct { 6 | ProductID uuid.UUID `json:"productID" binding:"required"` 7 | Price float64 `json:"price" binding:"required"` 8 | Discount int32 `json:"discount" binding:"required"` 9 | Description string `json:"description" binding:"required"` 10 | Name string `json:"name" binding:"required"` 11 | } 12 | type CreateProduct interface { 13 | Create(command CreateProductCommand) error 14 | } 15 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/command/update_product_name.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "github.com/google/uuid" 4 | 5 | type UpdateProductNameCommand struct { 6 | ProductID uuid.UUID 7 | ProductName string `json:"productName"` 8 | } 9 | type UpdateProductName interface { 10 | Update(command UpdateProductNameCommand) error 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/persistence/dao/product.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/id" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/entities" 6 | ) 7 | 8 | type ProductDAO interface { 9 | GetProductByID(productID id.ID) (entities.Product, error) 10 | CreateProduct(product entities.Product, tx interface{}) error 11 | UpdateProduct(product entities.Product, tx interface{}) error 12 | } 13 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/persistence/filters/filters.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 4 | 5 | type GetAllProductsFilters struct { 6 | filters.BaseFilters 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/persistence/reader/product.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/filters" 6 | ) 7 | 8 | type ProductReader interface { 9 | GetAllProducts(filters filters.GetAllProductsFilters) ([]dto.Product, error) 10 | GetProductByName(name string) (dto.Product, error) 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/query/get_all_products.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/query" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 6 | ) 7 | 8 | type GetAllProductsQuery struct { 9 | query.BaseListQueryParams 10 | } 11 | 12 | type GetAllProducts interface { 13 | Get(query GetAllProductsQuery) (dto.Products, error) 14 | } 15 | -------------------------------------------------------------------------------- /order/internal/application/product/interfaces/query/get_product_by_name.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 5 | ) 6 | 7 | type GetProductByNameQuery struct { 8 | Name string 9 | } 10 | type GetProductByName interface { 11 | Get(query GetProductByNameQuery) (dto.Product, error) 12 | } 13 | -------------------------------------------------------------------------------- /order/internal/application/product/query/get_all_products.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/dto" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/logger" 7 | baseFilters "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/filters" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/filters" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/reader" 11 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/query" 12 | ) 13 | 14 | type GetAllProductsImpl struct { 15 | DAO reader.ProductReader 16 | query.GetAllProducts 17 | logger.Logger 18 | } 19 | 20 | func (interactor *GetAllProductsImpl) Get(q query.GetAllProductsQuery) (dto.Products, error) { 21 | products, err := interactor.DAO.GetAllProducts( 22 | filters.GetAllProductsFilters{BaseFilters: baseFilters.BaseFilters{Limit: q.Limit, Offset: q.Offset, Order: q.Order}}, 23 | ) 24 | if err != nil { 25 | return dto.Products{}, err 26 | } 27 | length := len(products) 28 | interactor.Logger.Info( 29 | fmt.Sprintf("Get all products, count: %d, order: %s, limit: %d, offset: %d", 30 | length, q.Order, q.Limit, q.Offset, 31 | )) 32 | return dto.Products{ 33 | Products: products, 34 | BaseSequence: base.BaseSequence{Limit: q.Limit, Offset: q.Offset, Order: q.Order, Count: uint(length)}, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /order/internal/application/product/query/get_product_by_name.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/reader" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/query" 7 | ) 8 | 9 | type GetProductByNameImpl struct { 10 | DAO reader.ProductReader 11 | query.GetProductByName 12 | } 13 | 14 | func (interactor *GetProductByNameImpl) Get(query query.GetProductByNameQuery) (dto.Product, error) { 15 | productByName, err := interactor.DAO.GetProductByName(query.Name) 16 | if err != nil { 17 | return dto.Product{}, err 18 | } 19 | return productByName, nil 20 | } 21 | -------------------------------------------------------------------------------- /order/internal/application/relay/dto/outbox.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "github.com/google/uuid" 4 | 5 | type Message struct { 6 | ID uuid.UUID 7 | Exchange string 8 | Route string 9 | Payload []byte 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/application/relay/interactors/relay.go: -------------------------------------------------------------------------------- 1 | package interactors 2 | 3 | import ( 4 | "context" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/broker" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/interactors" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/persistence/dao" 8 | "github.com/google/uuid" 9 | "time" 10 | ) 11 | 12 | type RelayImpl struct { 13 | broker.MessageBroker 14 | dao.OutboxDAO 15 | interactors.Relay 16 | } 17 | 18 | func (r *RelayImpl) SendMessagesToBroker() { 19 | messages, err := r.GetAllNonProcessedMessages() 20 | if err != nil { 21 | return 22 | } 23 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 24 | defer cancel() 25 | 26 | processedIDs := make([]uuid.UUID, len(messages)) 27 | for _, m := range messages { 28 | if err := r.PublishMessage(ctx, m.Exchange, m.Route, m.Payload); err != nil { 29 | continue 30 | } 31 | processedIDs = append(processedIDs, m.ID) 32 | } 33 | if len(processedIDs) != 0 { 34 | err = r.OutboxDAO.UpdateMessage(processedIDs) 35 | if err != nil { 36 | return 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /order/internal/application/relay/interfaces/interactors/relay.go: -------------------------------------------------------------------------------- 1 | package interactors 2 | 3 | type Relay interface { 4 | SendMessagesToBroker() 5 | } 6 | -------------------------------------------------------------------------------- /order/internal/application/relay/interfaces/persistence/dao/outbox.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/consts/outbox" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/dto" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type OutboxDAO interface { 10 | GetAllNonProcessedMessages() ([]dto.Message, error) 11 | UpdateMessage([]uuid.UUID) error 12 | UpdateStatusMessagesByAggregateID(aggregateID uuid.UUID, status outbox.EventStatus, tx interface{}) error 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/domain/common/aggregate/aggregate.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 4 | 5 | type AggregateRoot struct { 6 | Events []events.Event 7 | } 8 | 9 | func (r *AggregateRoot) RecordEvent(event events.Event) { 10 | r.Events = append(r.Events, event) 11 | } 12 | func (r *AggregateRoot) GetEvents() []events.Event { 13 | return r.Events 14 | } 15 | func (r *AggregateRoot) PullEvents() []events.Event { 16 | length := len(r.GetEvents()) 17 | cleared := make([]events.Event, length) 18 | copy(cleared, r.GetEvents()) 19 | r.Events = r.Events[0:] 20 | return cleared 21 | } 22 | -------------------------------------------------------------------------------- /order/internal/domain/common/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | type Event interface { 10 | Bytes() ([]byte, error) 11 | UniqueAggregateID() uuid.UUID 12 | } 13 | type BaseEvent struct { 14 | EventID uuid.UUID `json:"eventID,omitempty"` 15 | EventType string `json:"eventType"` 16 | EventTimeStamp time.Time `json:"eventTimeStamp,omitempty"` 17 | } 18 | 19 | func (BaseEvent) Create(eventType string) BaseEvent { 20 | return BaseEvent{ 21 | EventType: eventType, 22 | EventID: uuid.New(), 23 | EventTimeStamp: time.Now(), 24 | } 25 | } 26 | func Bytes(v any) ([]byte, error) { 27 | bin, err := json.Marshal(v) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return bin, nil 32 | } 33 | func (o *BaseEvent) Bytes() ([]byte, error) { 34 | return Bytes(o) 35 | } 36 | func (o *BaseEvent) UniqueAggregateID() uuid.UUID { 37 | return o.EventID 38 | } 39 | -------------------------------------------------------------------------------- /order/internal/domain/common/exceptions.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type CustomException struct { 4 | Message string 5 | Ctx string 6 | } 7 | 8 | func (e *CustomException) Error() string { 9 | return e.Message + " " + e.Ctx 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/domain/common/id/id.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | type ID interface { 4 | ToString() string 5 | } 6 | -------------------------------------------------------------------------------- /order/internal/domain/order/aggregate/aggregate.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "errors" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/aggregate" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/consts" 7 | order "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/entities" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/vo" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type PriceOrder float64 13 | 14 | type Order struct { 15 | aggregate.AggregateRoot 16 | vo.OrderID 17 | Products []order.OrderProduct 18 | ClientID uuid.UUID 19 | OrderStatus consts.OrderStatus 20 | PaymentMethod consts.PaymentMethod 21 | DeliveryAddressID uuid.UUID 22 | totalPrice PriceOrder 23 | vo.OrderInfo 24 | vo.OrderDeleted 25 | } 26 | 27 | func (Order) Create( 28 | orderID vo.OrderID, 29 | deliveryAddress uuid.UUID, 30 | client uuid.UUID, 31 | previousSerialNumber int, 32 | products []order.OrderProduct, 33 | ) (Order, error) { 34 | serialNumber, serialError := getCurrentSerialNumber(previousSerialNumber) 35 | if serialError != nil { 36 | return Order{}, errors.New(serialError.Error()) 37 | } 38 | return Order{ 39 | OrderID: orderID, 40 | OrderStatus: consts.New, 41 | Products: products, 42 | ClientID: client, 43 | DeliveryAddressID: deliveryAddress, 44 | PaymentMethod: consts.Online, 45 | OrderInfo: vo.OrderInfo{}.Create(serialNumber), 46 | OrderDeleted: vo.OrderDeleted{}.Create(), 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /order/internal/domain/order/aggregate/read.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/exceptions" 5 | "github.com/google/uuid" 6 | "strconv" 7 | ) 8 | 9 | func getCurrentSerialNumber(serialNumber int) (int, error) { 10 | if serialNumber > 100 || serialNumber < 0 { 11 | exception := exceptions.InvalidSerialNumber{}.Exception(strconv.Itoa(serialNumber)) 12 | return -1, &exception 13 | } 14 | if serialNumber == 100 { 15 | return 1, nil 16 | } 17 | return serialNumber + 1, nil 18 | } 19 | func (o *Order) GetTotalPrice() PriceOrder { 20 | var totalPrice float64 21 | var totalDiscount float64 22 | for _, orderProduct := range o.Products { 23 | totalPrice += orderProduct.Price 24 | totalDiscount += float64(orderProduct.Discount) 25 | } 26 | o.totalPrice = PriceOrder(totalPrice - ((totalDiscount / 100) * 100)) 27 | return o.totalPrice 28 | } 29 | func (o *Order) GetAllProductsIds() []uuid.UUID { 30 | ids := make([]uuid.UUID, len(o.Products)) 31 | for index, product := range o.Products { 32 | ids[index] = product.ProductID 33 | } 34 | return ids 35 | } 36 | -------------------------------------------------------------------------------- /order/internal/domain/order/aggregate/validate.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/exceptions" 4 | 5 | func (o *Order) PreprocessOrder() error { 6 | if err := o.checkNotDeleted(); err != nil { 7 | return err 8 | } 9 | if err := o.checkNotClosed(); err != nil { 10 | return err 11 | } 12 | return nil 13 | } 14 | func (o *Order) checkNotDeleted() error { 15 | if o.Deleted { 16 | exception := exceptions.OrderIsDeleted{}.Exception(o.OrderID.ToString()) 17 | return &exception 18 | } 19 | return nil 20 | } 21 | 22 | func (o *Order) checkNotClosed() error { 23 | if o.Closed { 24 | exception := exceptions.OrderIsClosed{}.Exception(o.OrderID.ToString()) 25 | return &exception 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /order/internal/domain/order/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | type OrderStatus string 4 | 5 | const ( 6 | New OrderStatus = "New" 7 | InProcessing OrderStatus = "InProcessing" 8 | Processed OrderStatus = "Processed" 9 | Delivered OrderStatus = "Delivered" 10 | Canceled OrderStatus = "Canceled" 11 | ) 12 | 13 | type PaymentMethod string 14 | 15 | const ( 16 | Online PaymentMethod = "Online" 17 | Card PaymentMethod = "Card" 18 | Cash PaymentMethod = "Cash" 19 | ) 20 | -------------------------------------------------------------------------------- /order/internal/domain/order/entities/orderAdress.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import "github.com/google/uuid" 4 | 5 | type OrderAddress struct { 6 | AddressID uuid.UUID 7 | FullAddress string 8 | } 9 | 10 | func (OrderAddress) Create(id uuid.UUID, address string) (OrderAddress, error) { 11 | return OrderAddress{ 12 | AddressID: id, 13 | FullAddress: address, 14 | }, nil 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/domain/order/entities/orderClient.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import "github.com/google/uuid" 4 | 5 | type OrderClient struct { 6 | ClientID uuid.UUID 7 | Username string 8 | } 9 | 10 | func (OrderClient) Create(id uuid.UUID, username string) (OrderClient, error) { 11 | return OrderClient{ 12 | ClientID: id, 13 | Username: username, 14 | }, nil 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/domain/order/entities/orderProduct.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import "github.com/google/uuid" 4 | 5 | type OrderProduct struct { 6 | ProductID uuid.UUID `json:"productID"` 7 | Name string `json:"name"` 8 | Price float64 `json:"price"` 9 | Discount int32 `json:"discount"` 10 | } 11 | 12 | func (OrderProduct) Create(productID uuid.UUID, price float64, discount int32, name string) (OrderProduct, error) { 13 | return OrderProduct{ProductID: productID, Price: price, Discount: discount, Name: name}, nil 14 | } 15 | func (o *OrderProduct) GetActualPrice() float64 { 16 | return o.Price - ((float64(o.Discount) / 100) * 100) 17 | } 18 | -------------------------------------------------------------------------------- /order/internal/domain/order/events/order_delete.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type OrderDeleted struct { 9 | events.BaseEvent 10 | OrderID uuid.UUID `json:"orderID"` 11 | } 12 | 13 | func (OrderDeleted) Create( 14 | orderID uuid.UUID, 15 | ) events.Event { 16 | return &OrderDeleted{ 17 | BaseEvent: events.BaseEvent{}.Create("OrderDeleted"), 18 | OrderID: orderID, 19 | } 20 | } 21 | func (o *OrderDeleted) Bytes() ([]byte, error) { 22 | return events.Bytes(o) 23 | } 24 | func (o *OrderDeleted) UniqueAggregateID() uuid.UUID { 25 | return o.OrderID 26 | } 27 | -------------------------------------------------------------------------------- /order/internal/domain/order/events/product_order_add.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type OrderAddProduct struct { 9 | events.BaseEvent 10 | OrderID uuid.UUID `json:"orderID"` 11 | ClientID uuid.UUID `json:"clientID"` 12 | ProductID uuid.UUID `json:"productID"` 13 | ProductName string `json:"productName"` 14 | ProductPrice float64 `json:"productPrice"` 15 | OrderPrice float64 `json:"orderPrice"` 16 | } 17 | 18 | func (OrderAddProduct) Create( 19 | orderID uuid.UUID, 20 | product uuid.UUID, 21 | productPrice float64, 22 | orderPrice float64, 23 | productName string, 24 | clientID uuid.UUID, 25 | ) events.Event { 26 | return &OrderAddProduct{ 27 | BaseEvent: events.BaseEvent{}.Create("OrderAddProduct"), 28 | OrderID: orderID, 29 | ProductID: product, 30 | ProductName: productName, 31 | ProductPrice: productPrice, 32 | OrderPrice: orderPrice, 33 | ClientID: clientID, 34 | } 35 | } 36 | func (o *OrderAddProduct) Bytes() ([]byte, error) { 37 | return events.Bytes(o) 38 | } 39 | func (o *OrderAddProduct) UniqueAggregateID() uuid.UUID { 40 | return o.OrderID 41 | } 42 | -------------------------------------------------------------------------------- /order/internal/domain/order/events/saga_event.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type OrderCreateSaga struct { 9 | events.BaseEvent 10 | OrderID uuid.UUID `json:"orderID"` 11 | ProductsID []uuid.UUID `json:"productsIDs"` 12 | TotalPrice float64 `json:"totalPrice"` 13 | } 14 | 15 | func (OrderCreateSaga) Create(orderID uuid.UUID, price float64, productsID []uuid.UUID) events.Event { 16 | return &OrderCreateSaga{ 17 | BaseEvent: events.BaseEvent{}.Create("OrderCreateSaga"), 18 | ProductsID: productsID, 19 | OrderID: orderID, 20 | TotalPrice: price, 21 | } 22 | } 23 | func (o *OrderCreateSaga) Bytes() ([]byte, error) { 24 | return events.Bytes(o) 25 | } 26 | func (o *OrderCreateSaga) UniqueAggregateID() uuid.UUID { 27 | return o.OrderID 28 | } 29 | -------------------------------------------------------------------------------- /order/internal/domain/order/vo/order_delete.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import "time" 4 | 5 | type OrderDeleted struct { 6 | Deleted bool `json:"deleted"` 7 | DeleteAt *time.Time `json:"delete_at"` 8 | } 9 | 10 | func (OrderDeleted) Create() OrderDeleted { 11 | return OrderDeleted{Deleted: false, DeleteAt: nil} 12 | } 13 | -------------------------------------------------------------------------------- /order/internal/domain/order/vo/order_id.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import "github.com/google/uuid" 4 | 5 | type OrderID struct { 6 | Value uuid.UUID 7 | } 8 | 9 | func (id OrderID) ToString() string { 10 | return id.Value.String() 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/domain/order/vo/order_info.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import "time" 4 | 5 | type OrderInfo struct { 6 | Date time.Time 7 | SerialNumber int 8 | Closed bool 9 | } 10 | 11 | func (OrderInfo) Create(serialNumber int) OrderInfo { 12 | return OrderInfo{SerialNumber: serialNumber, Date: time.Now(), Closed: false} 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/domain/product/entities/product.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/exceptions" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/vo" 6 | "regexp" 7 | ) 8 | 9 | type Product struct { 10 | vo.ProductID 11 | Price vo.ProductPrice 12 | Name string 13 | Discount vo.ProductDiscount 14 | Quantity int32 15 | Description string 16 | Availability bool 17 | } 18 | 19 | func (Product) Create( 20 | productID vo.ProductID, 21 | price vo.ProductPrice, 22 | discount vo.ProductDiscount, 23 | description string, 24 | name string, 25 | ) (Product, error) { 26 | return Product{ 27 | ProductID: productID, 28 | Price: price, 29 | Name: name, 30 | Discount: discount, 31 | Quantity: 1, 32 | Description: description, 33 | Availability: true, 34 | }, nil 35 | } 36 | func (product *Product) UpdateName(name string) error { 37 | matched, err := regexp.MatchString("^[A-Z].*", name) 38 | if err != nil { 39 | return err 40 | } 41 | if !matched { 42 | exception := exceptions.InvalidProductNameUpdate{}.Exception(name) 43 | return &exception 44 | } 45 | product.Name = name 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /order/internal/domain/product/exceptions/product.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common" 6 | ) 7 | 8 | type InvalidPriceProductCreation struct { 9 | common.CustomException 10 | } 11 | type InvalidDiscountProductCreation struct { 12 | common.CustomException 13 | } 14 | type InvalidProductNameUpdate struct { 15 | common.CustomException 16 | } 17 | 18 | func (e InvalidPriceProductCreation) Exception(context string) InvalidPriceProductCreation { 19 | return InvalidPriceProductCreation{ 20 | CustomException: common.CustomException{ 21 | Message: "Price cannot be negative;", 22 | Ctx: fmt.Sprintf("price: `%s`", context), 23 | }} 24 | } 25 | func (e InvalidDiscountProductCreation) Exception(context string) InvalidDiscountProductCreation { 26 | return InvalidDiscountProductCreation{ 27 | CustomException: common.CustomException{ 28 | Message: "Discount must be in from 0 to 99;", 29 | Ctx: fmt.Sprintf("discount: `%s`", context), 30 | }} 31 | } 32 | func (e InvalidProductNameUpdate) Exception(context string) InvalidProductNameUpdate { 33 | return InvalidProductNameUpdate{ 34 | CustomException: common.CustomException{ 35 | Message: "Product name must be start with capital letter", 36 | Ctx: fmt.Sprintf("product name: `%s`", context), 37 | }} 38 | } 39 | -------------------------------------------------------------------------------- /order/internal/domain/product/vo/product_discount.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/exceptions" 5 | "strconv" 6 | ) 7 | 8 | type ProductDiscount struct { 9 | Value int32 10 | } 11 | 12 | func (ProductDiscount) Create(discount int32) (ProductDiscount, error) { 13 | if discount < 0 || discount > 99 { 14 | discountError := exceptions.InvalidDiscountProductCreation{}.Exception(strconv.Itoa(int(discount))) 15 | return ProductDiscount{}, &discountError 16 | } 17 | return ProductDiscount{Value: discount}, nil 18 | } 19 | -------------------------------------------------------------------------------- /order/internal/domain/product/vo/product_id.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/id" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type ProductID struct { 9 | id.ID 10 | Value uuid.UUID 11 | } 12 | 13 | func GetProductIDs(productIDs []uuid.UUID) []id.ID { 14 | ids := make([]id.ID, len(productIDs)) 15 | for i, productID := range productIDs { 16 | ids[i] = ProductID{Value: productID} 17 | } 18 | return ids 19 | } 20 | func (id ProductID) ToString() string { 21 | return id.Value.String() 22 | } 23 | -------------------------------------------------------------------------------- /order/internal/domain/product/vo/product_price.go: -------------------------------------------------------------------------------- 1 | package vo 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/exceptions" 5 | "strconv" 6 | ) 7 | 8 | type ProductPrice struct { 9 | Value float64 10 | } 11 | 12 | func (ProductPrice) Create(price float64) (ProductPrice, error) { 13 | if price < 0 { 14 | priceError := exceptions.InvalidPriceProductCreation{}.Exception(strconv.Itoa(int(price))) 15 | return ProductPrice{}, &priceError 16 | } 17 | return ProductPrice{Value: price}, nil 18 | } 19 | -------------------------------------------------------------------------------- /order/internal/infrastructure/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/config" 5 | "github.com/redis/go-redis/v9" 6 | ) 7 | 8 | type Cache struct { 9 | *redis.Client 10 | } 11 | 12 | func NewClient(redisConf config.RedisConfig) Cache { 13 | return Cache{Client: redis.NewClient( 14 | &redis.Options{ 15 | Addr: redisConf.FullAddress(), 16 | Password: redisConf.Password, 17 | DB: redisConf.DB, 18 | }, 19 | )} 20 | } 21 | -------------------------------------------------------------------------------- /order/internal/infrastructure/cache/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type RedisConfig struct { 6 | Host string `json:"host"` 7 | Port int `json:"port"` 8 | Password string `json:"password"` 9 | DB int `json:"db"` 10 | } 11 | 12 | func (r *RedisConfig) FullAddress() string { 13 | return fmt.Sprintf("%s:%d", r.Host, r.Port) 14 | } 15 | -------------------------------------------------------------------------------- /order/internal/infrastructure/cache/dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache" 4 | 5 | type BaseRedisDAO struct { 6 | cache.Cache 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/infrastructure/cache/dao/order/cache.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/dto" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/dao" 9 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/dao" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type CacheDAOImpl struct { 14 | dao.OrderCacheDAO 15 | base.BaseRedisDAO 16 | } 17 | 18 | func (dao *CacheDAOImpl) GetOrder(orderID uuid.UUID) dto.Order { 19 | keys, _ := dao.Client.Keys(context.Background(), fmt.Sprintf("order:*:%s", orderID)).Result() 20 | for _, key := range keys { 21 | orderData, err := dao.Client.Get(context.Background(), key).Result() 22 | if err != nil { 23 | break 24 | } 25 | view := dto.Order{} 26 | 27 | err = json.Unmarshal([]byte(orderData), &view) 28 | if err != nil { 29 | break 30 | } 31 | return view //nolint 32 | } 33 | return dto.Order{} 34 | } 35 | func (dao *CacheDAOImpl) SaveOrder(order dto.Order) error { 36 | marshal, err := json.Marshal(order) 37 | if err != nil { 38 | return err 39 | } 40 | status := dao.Client.Set( 41 | context.Background(), 42 | fmt.Sprintf("order:%s:%s", order.ClientID, order.OrderID), 43 | marshal, 44 | 0, 45 | ) 46 | return status.Err() 47 | } 48 | func (dao *CacheDAOImpl) DeleteOrder(orderID uuid.UUID) error { 49 | keys, _ := dao.Client.Keys(context.Background(), fmt.Sprintf("order:*:%s", orderID)).Result() 50 | for _, key := range keys { 51 | _, err := dao.Client.Del(context.Background(), key).Result() 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /order/internal/infrastructure/cache/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache" 4 | 5 | type BaseRedisReader struct { 6 | cache.Cache 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/infrastructure/config/load-config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | const DefaultConfigPath = "./configs/app/dev.toml" 10 | 11 | func LoadConfig(val interface{}, absolutePath string, relativePath string) { 12 | relativeEnv := getEnv("CONFIG_PATH", "") 13 | if relativeEnv == "" { 14 | relativeEnv = DefaultConfigPath 15 | } 16 | if relativePath != "" && getEnv("CONFIG_PATH", "") == "" { 17 | relativeEnv = relativePath 18 | } 19 | 20 | var pathConf string 21 | if absolutePath != "" { 22 | pathConf = filepath.Join(absolutePath, relativeEnv) 23 | } else { 24 | pathConf = relativeEnv 25 | } 26 | _, err := toml.DecodeFile(pathConf, val) 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | func getEnv(key string, defaultVal string) string { 32 | if value, exists := os.LookupEnv(key); exists { 33 | return value 34 | } 35 | 36 | return defaultVal 37 | } 38 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type DBConfig struct { 8 | Host string `toml:"host"` 9 | Port int `toml:"port"` 10 | Database string `toml:"database"` 11 | User string `toml:"user"` 12 | Password string `toml:"password"` 13 | Migration bool `toml:"migration"` 14 | Logging bool `toml:"logging"` 15 | MaxIdleConnection int `toml:"max_idle_connection"` 16 | } 17 | 18 | func (conf *DBConfig) FullDNS() string { 19 | return fmt.Sprintf( 20 | "host=%s user=%s password=%s dbname=%s port=%d", 21 | conf.Host, conf.User, conf.Password, conf.Database, conf.Port, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/base.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import "gorm.io/gorm" 4 | 5 | type BaseGormDAO struct { 6 | Session *gorm.DB 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/order/convertors.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | order "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/entities" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 6 | ) 7 | 8 | func ConvertProductsModelsToOrderEntity(models []models.Product) []order.OrderProduct { 9 | products := make([]order.OrderProduct, len(models)) 10 | for index, model := range models { 11 | products[index] = order.OrderProduct{ 12 | ProductID: model.ID, 13 | Price: model.Price, 14 | Name: model.Name, 15 | Discount: model.Discount, 16 | } 17 | } 18 | return products 19 | } 20 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "errors" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/exceptions" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/dao" 7 | order "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/entities" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 9 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 10 | "github.com/google/uuid" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type DAOImpl struct { 15 | dao.OrderDAO 16 | repo.BaseGormRepo 17 | } 18 | 19 | func (dao *DAOImpl) GetProductsByIDs(productIDs []uuid.UUID) ([]order.OrderProduct, error) { 20 | ids := make([]string, len(productIDs)) 21 | for index, productID := range productIDs { 22 | ids[index] = productID.String() 23 | } 24 | var productsModel []models.Product 25 | result := dao.Session.Where("id IN ?", productIDs).Find(&productsModel) 26 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 27 | exception := exceptions.ProductIDsNotExist{}.Exception(ids) 28 | return []order.OrderProduct{}, &exception 29 | } 30 | if result.Error != nil { 31 | return []order.OrderProduct{}, result.Error 32 | } 33 | return ConvertProductsModelsToOrderEntity(productsModel), nil 34 | } 35 | 36 | func (dao *DAOImpl) DeleteOrder(orderID uuid.UUID, tx interface{}) error { 37 | return tx.(*gorm.DB). 38 | Where("id = ?", orderID). 39 | Delete(&models.Order{}).Error 40 | } 41 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/order/saga.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/consts" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/dao" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 7 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 8 | "github.com/google/uuid" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type SagaDAOImpl struct { 13 | dao.OrderSagaDAO 14 | repo.BaseGormRepo 15 | } 16 | 17 | func (dao *SagaDAOImpl) CheckSagaCompletion(orderID uuid.UUID) (bool, error) { 18 | var count int64 19 | res := dao.Session.Model(&models.Order{}).Where("saga_status = ? AND id = ?", consts.Pending, orderID).Count(&count) 20 | return count > 0, res.Error 21 | } 22 | func (dao *SagaDAOImpl) UpdateOrderSagaStatus(orderID uuid.UUID, status string, tx interface{}) error { 23 | return tx.(*gorm.DB). 24 | Model(&models.Order{}). 25 | Where("id = ?", orderID). 26 | UpdateColumn("saga_status", status).Error 27 | } 28 | func (dao *SagaDAOImpl) DeleteOrder(orderID uuid.UUID, tx interface{}) error { 29 | return tx.(*gorm.DB). 30 | Where("id = ?", orderID). 31 | Delete(&models.Order{}).Error 32 | } 33 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/outbox/convertors.go: -------------------------------------------------------------------------------- 1 | package outbox 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/dto" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 7 | ) 8 | 9 | func ConvertOutboxModelToDTO(model models.Outbox) (dto.Message, error) { 10 | b, err := json.Marshal(model.Payload) 11 | if err != nil { 12 | return dto.Message{}, err 13 | } 14 | return dto.Message{ 15 | ID: model.ID, 16 | Exchange: model.Exchange, 17 | Route: model.Route, 18 | Payload: b, 19 | }, nil 20 | } 21 | func ConvertOutboxModelsToDTOs(models []models.Outbox) []dto.Message { 22 | messages := make([]dto.Message, len(models)) 23 | for _, model := range models { 24 | message, err := ConvertOutboxModelToDTO(model) 25 | if err != nil { 26 | continue 27 | } 28 | messages = append(messages, message) 29 | } 30 | return messages 31 | } 32 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/outbox/outbox.go: -------------------------------------------------------------------------------- 1 | package outbox 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/consts/outbox" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/dto" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/persistence/dao" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 8 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 9 | "github.com/google/uuid" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type DAOImpl struct { 14 | dao.OutboxDAO 15 | repo.BaseGormRepo 16 | } 17 | 18 | func (dao *DAOImpl) GetAllNonProcessedMessages() ([]dto.Message, error) { 19 | var messages []models.Outbox 20 | result := dao.Session. 21 | Where("event_status = ?", outbox.Awaiting). 22 | Find(&messages) 23 | if result.Error != nil { 24 | return []dto.Message{}, nil 25 | } 26 | return ConvertOutboxModelsToDTOs(messages), nil 27 | } 28 | func (dao *DAOImpl) UpdateMessage(ids []uuid.UUID) error { 29 | return dao.Session. 30 | Model(&models.Outbox{}). 31 | Where("id IN ?", ids). 32 | UpdateColumn("event_status", outbox.Processed).Error 33 | } 34 | func (dao *DAOImpl) UpdateStatusMessagesByAggregateID(aggregateID uuid.UUID, status outbox.EventStatus, tx interface{}) error { 35 | return tx.(*gorm.DB). 36 | Model(&models.Outbox{}). 37 | Where("aggregate_id = ?", aggregateID). 38 | UpdateColumn("event_status", status).Error 39 | } 40 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/product/convertors.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/entities" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/vo" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 7 | ) 8 | 9 | func ConvertProductModelToEntity(model models.Product) entities.Product { 10 | return entities.Product{ 11 | ProductID: vo.ProductID{Value: model.ID}, 12 | Price: vo.ProductPrice{Value: model.Price}, 13 | Name: model.Name, 14 | Discount: vo.ProductDiscount{Value: model.Discount}, 15 | Quantity: model.Quantity, 16 | Description: model.Description, 17 | Availability: model.Availability, 18 | } 19 | } 20 | func ConvertProductEntityToModel(entity entities.Product) models.Product { 21 | return models.Product{ 22 | Base: models.Base{ID: entity.ProductID.Value}, 23 | Price: entity.Price.Value, 24 | Name: entity.Name, 25 | Discount: entity.Discount.Value, 26 | Quantity: entity.Quantity, 27 | Description: entity.Description, 28 | Availability: entity.Availability, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/dao/product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "errors" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/exceptions" 6 | appDAO "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/dao" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/id" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/entities" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 10 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type DAOImpl struct { 15 | repo.BaseGormRepo 16 | appDAO.ProductDAO 17 | } 18 | 19 | func (repo *DAOImpl) GetProductByID(productID id.ID) (entities.Product, error) { 20 | var productModel models.Product 21 | result := repo.Session.Where("id = ?", productID.ToString()).First(&productModel) 22 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 23 | exception := exceptions.ProductIDNotExist{}.Exception(productID.ToString()) 24 | return entities.Product{}, &exception 25 | } 26 | if result.Error != nil { 27 | return entities.Product{}, result.Error 28 | } 29 | return ConvertProductModelToEntity(productModel), nil 30 | } 31 | func (repo *DAOImpl) UpdateProduct(product entities.Product, tx interface{}) error { 32 | productModel := ConvertProductEntityToModel(product) 33 | result := tx.(*gorm.DB).Where("id = ?", productModel.ID).Updates(productModel) 34 | if result.Error != nil { 35 | return result.Error 36 | } 37 | return nil 38 | } 39 | func (repo *DAOImpl) CreateProduct(product entities.Product, tx interface{}) error { 40 | model := ConvertProductEntityToModel(product) 41 | return tx.(*gorm.DB).Create(&model).Error 42 | } 43 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/config" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 6 | "gorm.io/driver/postgres" 7 | "gorm.io/gorm" 8 | gormLogger "gorm.io/gorm/logger" 9 | ) 10 | 11 | func BuildConnection(logger logger.Logger, config config.DBConfig) *gorm.DB { 12 | gormConfig := gorm.Config{} 13 | if !config.Logging { 14 | gormConfig.Logger = gormLogger.Default.LogMode(gormLogger.Silent) 15 | } else { 16 | gormConfig.Logger = logger.GetGormLogger() 17 | } 18 | db, err := gorm.Open(postgres.Open(config.FullDNS()), &gormConfig) 19 | sqlDB, errSQL := db.DB() 20 | if errSQL != nil { 21 | panic(errSQL) 22 | } 23 | sqlDB.SetMaxIdleConns(config.MaxIdleConnection) 24 | if err == nil { 25 | if config.Migration { 26 | migrate(db) 27 | } 28 | 29 | return db 30 | } 31 | panic(err.Error()) 32 | } 33 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/migrations.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func migrate(db *gorm.DB) { 9 | _ = db.AutoMigrate(&models.Product{}, &models.Order{}, &models.Outbox{}) 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/models/base.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Base struct { 9 | ID uuid.UUID `gorm:"index;type:uuid;primary_key;default:uuid_generate_v4()"` 10 | CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP; not null"` 11 | UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP; not null"` 12 | DeletedAt *time.Time `gorm:"default:null"` 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/models/order.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type Order struct { 8 | Base 9 | OrderStatus string 10 | ClientID uuid.UUID 11 | PaymentMethod string 12 | AddressID uuid.UUID 13 | Closed bool 14 | SerialNumber int 15 | Deleted bool `gorm:"default:false"` 16 | SagaStatus string `gorm:"default:Pending"` 17 | 18 | Products []Product `gorm:"many2many:order_products;"` 19 | } 20 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/models/outbox.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/google/uuid" 4 | 5 | type Outbox struct { 6 | Base 7 | Exchange string 8 | Route string 9 | Payload string `gorm:"type:jsonb;"` 10 | AggregateID uuid.UUID 11 | EventStatus int `gorm:"default:1"` 12 | } 13 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/models/product.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Product struct { 4 | Base 5 | Price float64 6 | Name string `gorm:"unique"` 7 | Discount int32 8 | Quantity int32 9 | Description string 10 | Availability bool 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/reader/product/convertors.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 6 | ) 7 | 8 | func ConvertProductModelToDTO(model models.Product) dto.Product { 9 | return dto.Product{ 10 | ID: model.ID, 11 | Name: model.Name, 12 | Price: model.Price, 13 | Discount: model.Discount, 14 | Quantity: model.Quantity, 15 | Description: model.Description, 16 | Availability: model.Availability, 17 | CreatedAt: model.CreatedAt, 18 | } 19 | } 20 | func ConvertProductModelsToDTOs(models []models.Product) []dto.Product { 21 | products := make([]dto.Product, len(models)) 22 | for i, product := range models { 23 | products[i] = ConvertProductModelToDTO(product) 24 | } 25 | return products 26 | } 27 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/reader/product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/dto" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/exceptions" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/filters" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/reader" 10 | db "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/dao" 11 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type ReaderImpl struct { 16 | db.BaseGormDAO 17 | reader.ProductReader 18 | } 19 | 20 | func (dao *ReaderImpl) GetAllProducts(filters filters.GetAllProductsFilters) ([]dto.Product, error) { 21 | var products []models.Product 22 | result := dao.Session. 23 | Limit(int(filters.Limit)). 24 | Offset(int(filters.Offset)). 25 | Order(fmt.Sprintf("price %s", filters.Order)). 26 | Find(&products) 27 | if result.Error != nil { 28 | return []dto.Product{}, result.Error 29 | } 30 | return ConvertProductModelsToDTOs(products), nil 31 | } 32 | func (dao *ReaderImpl) GetProductByName(name string) (dto.Product, error) { 33 | var product models.Product 34 | result := dao.Session.Where("name = ?", name).First(&product) 35 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 36 | exception := exceptions.ProductNameNotExist{}.Exception(name) 37 | return dto.Product{}, &exception 38 | } 39 | if result.Error != nil { 40 | return dto.Product{}, result.Error 41 | } 42 | return ConvertProductModelToDTO(product), nil 43 | } 44 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/reader/utils.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 4 | 5 | func CalculateTotalProductPrice(product models.Product) float64 { 6 | return product.Price - ((float64(product.Discount) / 100) * 100) 7 | } 8 | func CalculateTotalOrderPrice(products []models.Product) float64 { 9 | var totalPrice float64 10 | for _, product := range products { 11 | totalPrice += CalculateTotalProductPrice(product) 12 | } 13 | return totalPrice 14 | } 15 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/repo/base.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import "gorm.io/gorm" 4 | 5 | type BaseGormRepo struct { 6 | Session *gorm.DB 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/repo/outbox/convertors/convert_order.go: -------------------------------------------------------------------------------- 1 | package convertors 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/consts/outbox" 5 | o "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/events" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 7 | "reflect" 8 | ) 9 | 10 | func OrdersEventsHandler(outboxes *[]models.Outbox, payload PayloadEnhanced) bool { 11 | if payload.reflect == reflect.TypeOf(new(o.OrderCreated)) { 12 | *outboxes = append(*outboxes, models.Outbox{ 13 | Exchange: "Orders", 14 | Route: "Order.Create", 15 | Payload: payload.payload, 16 | EventStatus: outbox.Sagas, 17 | AggregateID: payload.eventUniqueID, 18 | }) 19 | return true 20 | } 21 | if payload.reflect == reflect.TypeOf(new(o.OrderAddProduct)) { 22 | *outboxes = append(*outboxes, models.Outbox{ 23 | Exchange: "Orders", 24 | Route: "Order.AddProduct", 25 | Payload: payload.payload, 26 | EventStatus: outbox.Sagas, 27 | AggregateID: payload.eventUniqueID, 28 | }) 29 | return true 30 | } 31 | if payload.reflect == reflect.TypeOf(new(o.OrderDeleted)) { 32 | *outboxes = append(*outboxes, models.Outbox{ 33 | Exchange: "Orders", 34 | Route: "Order.Delete", 35 | Payload: payload.payload, 36 | AggregateID: payload.eventUniqueID, 37 | }) 38 | return true 39 | } 40 | if payload.reflect == reflect.TypeOf(new(o.OrderCreateSaga)) { 41 | *outboxes = append(*outboxes, models.Outbox{ 42 | Exchange: "Orders", 43 | Route: "Order.Saga.Create", 44 | Payload: payload.payload, 45 | AggregateID: payload.eventUniqueID, 46 | }) 47 | return true 48 | } 49 | 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/repo/outbox/convertors/convertors.go: -------------------------------------------------------------------------------- 1 | package convertors 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/models" 6 | "github.com/google/uuid" 7 | "reflect" 8 | ) 9 | 10 | type PayloadEnhanced struct { 11 | payload string 12 | reflect reflect.Type 13 | eventUniqueID uuid.UUID 14 | } 15 | type EventToOutbox struct { 16 | payloads []PayloadEnhanced 17 | } 18 | 19 | func (e EventToOutbox) Create(events []events.Event) (EventToOutbox, error) { 20 | payloads := make([]PayloadEnhanced, len(events)) 21 | for _, event := range events { 22 | binary, binaryErr := event.Bytes() 23 | if binaryErr != nil { 24 | return EventToOutbox{}, binaryErr 25 | } 26 | 27 | payloads = append(payloads, PayloadEnhanced{payload: string(binary), reflect: reflect.TypeOf(event), eventUniqueID: event.UniqueAggregateID()}) 28 | } 29 | return EventToOutbox{payloads: payloads}, nil 30 | } 31 | func (e *EventToOutbox) Convert() []models.Outbox { 32 | var modelsOutbox []models.Outbox 33 | for _, payload := range e.payloads { 34 | if OrdersEventsHandler(&modelsOutbox, payload) { 35 | continue 36 | } 37 | } 38 | return modelsOutbox 39 | } 40 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/repo/outbox/outbox.go: -------------------------------------------------------------------------------- 1 | package outbox 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/repo" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common/events" 6 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo/outbox/convertors" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type RepoImpl struct { 12 | base.BaseGormRepo 13 | repo.OutboxRepo 14 | } 15 | 16 | func (repo *RepoImpl) AddEvents(events []events.Event, tx interface{}) error { 17 | converter, err := convertors.EventToOutbox{}.Create(events) 18 | if err != nil { 19 | return err 20 | } 21 | models := converter.Convert() 22 | result := tx.(*gorm.DB).Create(&models) 23 | if result.Error != nil { 24 | return result.Error 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /order/internal/infrastructure/db/uow/uow.go: -------------------------------------------------------------------------------- 1 | package uow 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type GormUoW struct { 9 | persistence.UoW 10 | Session *gorm.DB 11 | tx *gorm.DB 12 | } 13 | 14 | func (uow *GormUoW) Get() persistence.UoW { 15 | return BuildGormUoW(uow.Session) 16 | } 17 | 18 | func (uow *GormUoW) StartTx() any { 19 | uow.tx = uow.Session.Begin() 20 | return uow.tx 21 | } 22 | 23 | func (uow *GormUoW) GetTx() any { 24 | if uow.tx != nil { 25 | return uow.tx 26 | } 27 | uow.tx = uow.Session.Begin() 28 | return uow.tx 29 | } 30 | func (uow *GormUoW) Commit() error { 31 | uow.tx.Commit() 32 | uow.tx = nil 33 | return nil 34 | } 35 | func (uow *GormUoW) Rollback() { 36 | uow.tx.Rollback() 37 | uow.tx = nil 38 | } 39 | func BuildGormUoW(conn *gorm.DB) persistence.UoW { 40 | return &GormUoW{Session: conn} 41 | } 42 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/di.go: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/cache" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/db" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/interactors" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/logger" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/mediator" 9 | messagebroker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/messageBroker" 10 | "go.uber.org/fx" 11 | ) 12 | 13 | var Module = fx.Module( 14 | "infrastructure.di", 15 | fx.Options( 16 | db.Module, 17 | interactors.Module, 18 | messagebroker.Module, 19 | cache.Module, 20 | logger.Module, 21 | mediator.Module, 22 | ), 23 | ) 24 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/cache/dao" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/cache/reader" 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Options( 11 | fx.Provide(cache.NewClient), 12 | dao.Module, 13 | reader.Module, 14 | ) 15 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/cache/dao/order.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/dao" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache" 6 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/dao" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/dao/order" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | func NewOrderCacheDAO(cache cache.Cache) dao.OrderCacheDAO { 12 | return &order.CacheDAOImpl{ 13 | BaseRedisDAO: base.BaseRedisDAO{Cache: cache}, 14 | } 15 | } 16 | 17 | var Module = fx.Provide( 18 | NewOrderCacheDAO, 19 | ) 20 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/cache/reader/order.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/reader" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache" 6 | r "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/reader" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/reader/order" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | func NewOrderCacheReader(cache cache.Cache) reader.OrderCacheReader { 12 | return &order.CacheReaderImpl{ 13 | BaseRedisReader: r.BaseRedisReader{Cache: cache}, 14 | } 15 | } 16 | 17 | var Module = fx.Provide( 18 | NewOrderCacheReader, 19 | ) 20 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/dao" 6 | base "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/uow" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/db/orders" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/db/outbox" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/db/product" 11 | "go.uber.org/fx" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func NewBaseRepo(conn *gorm.DB) base.BaseGormRepo { 16 | return base.BaseGormRepo{Session: conn} 17 | } 18 | func NewBaseDAO(conn *gorm.DB) dao.BaseGormDAO { 19 | return dao.BaseGormDAO{Session: conn} 20 | } 21 | 22 | var Module = fx.Options( 23 | product.Module, 24 | orders.Module, 25 | fx.Provide( 26 | uow.BuildGormUoW, 27 | db.BuildConnection, 28 | NewBaseRepo, 29 | NewBaseDAO, 30 | ), 31 | 32 | outbox.Module, 33 | ) 34 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/db/orders/order.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | appDAO "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/dao" 5 | appRepo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/persistence/repo" 6 | orderDAO "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/dao/order" 7 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo/order" 9 | "go.uber.org/fx" 10 | ) 11 | 12 | func BuildOrderRepo(base repo.BaseGormRepo) appRepo.OrderRepo { 13 | return &order.RepoImpl{ 14 | BaseGormRepo: base, 15 | } 16 | } 17 | func BuildOrderDAO(base repo.BaseGormRepo) appDAO.OrderDAO { 18 | return &orderDAO.DAOImpl{ 19 | BaseGormRepo: base, 20 | } 21 | } 22 | func BuildOrderSagaDAO(base repo.BaseGormRepo) appDAO.OrderSagaDAO { 23 | return &orderDAO.SagaDAOImpl{ 24 | BaseGormRepo: base, 25 | } 26 | } 27 | 28 | var Module = fx.Provide( 29 | BuildOrderRepo, 30 | BuildOrderDAO, 31 | 32 | BuildOrderSagaDAO, 33 | ) 34 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/db/outbox/outbox.go: -------------------------------------------------------------------------------- 1 | package outbox 2 | 3 | import ( 4 | appRepo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence/repo" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/persistence/dao" 6 | relay "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/dao/outbox" 7 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo/outbox" 9 | "go.uber.org/fx" 10 | ) 11 | 12 | func BuildOutboxRepo(base repo.BaseGormRepo) appRepo.OutboxRepo { 13 | return &outbox.RepoImpl{BaseGormRepo: base} 14 | } 15 | func BuildOutboxDAO(base repo.BaseGormRepo) dao.OutboxDAO { 16 | return &relay.DAOImpl{BaseGormRepo: base} 17 | } 18 | 19 | var Module = fx.Provide( 20 | BuildOutboxRepo, 21 | BuildOutboxDAO, 22 | ) 23 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/db/product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | appDAO "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/dao" 5 | appReader "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/reader" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/dao" 7 | productDAO "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/dao/product" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/reader/product" 9 | repo "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/repo" 10 | "go.uber.org/fx" 11 | ) 12 | 13 | func BuildProductRepo(base repo.BaseGormRepo) appDAO.ProductDAO { 14 | return &productDAO.DAOImpl{BaseGormRepo: base} 15 | } 16 | func BuildProductReader(base dao.BaseGormDAO) appReader.ProductReader { 17 | return &product.ReaderImpl{BaseGormDAO: base} 18 | } 19 | 20 | var Module = fx.Provide( 21 | BuildProductRepo, 22 | BuildProductReader, 23 | ) 24 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/interactors/interactors.go: -------------------------------------------------------------------------------- 1 | package interactors 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/interactors/order" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/interactors/product" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/di/factories/interactors/relay" 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Options( 11 | product.Module, 12 | order.Module, 13 | 14 | relay.Module, 15 | ) 16 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/interactors/product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/persistence" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/command" 6 | c "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/command" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/dao" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/persistence/reader" 9 | q "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/query" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/query" 11 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 12 | "go.uber.org/fx" 13 | ) 14 | 15 | func NewCreateProduct(dao dao.ProductDAO, uow persistence.UoW) c.CreateProduct { 16 | return &command.CreateProductImpl{ 17 | ProductDAO: dao, 18 | UoW: uow, 19 | } 20 | } 21 | func NewUpdateProductName(dao dao.ProductDAO, uow persistence.UoW) c.UpdateProductName { 22 | return &command.UpdateProductNameImpl{ 23 | ProductDAO: dao, 24 | UoW: uow, 25 | } 26 | } 27 | 28 | func NewGetALlProducts(dao reader.ProductReader, logger logger.Logger) q.GetAllProducts { 29 | return &query.GetAllProductsImpl{ 30 | DAO: dao, 31 | Logger: logger, 32 | } 33 | } 34 | func NewGetProductByName(dao reader.ProductReader) q.GetProductByName { 35 | return &query.GetProductByNameImpl{ 36 | DAO: dao, 37 | } 38 | } 39 | 40 | var Module = fx.Provide( 41 | NewCreateProduct, 42 | NewGetALlProducts, 43 | NewUpdateProductName, 44 | NewGetProductByName, 45 | ) 46 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/interactors/relay/relay.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/broker" 5 | impl "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interactors" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/interactors" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/persistence/dao" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | func NewOutboxRelay(dao dao.OutboxDAO, broker broker.MessageBroker) interactors.Relay { 12 | return &impl.RelayImpl{ 13 | OutboxDAO: dao, 14 | MessageBroker: broker, 15 | } 16 | } 17 | 18 | var Module = fx.Provide( 19 | NewOutboxRelay, 20 | ) 21 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Provide( 9 | logger.NewLogger, 10 | ) 11 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/mediator/params.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | import ( 4 | commandOrder "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/command" 5 | queryOrder "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/query" 6 | commandProduct "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/command" 7 | queryProduct "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/query" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | type Params struct { 12 | fx.In 13 | queryProduct.GetAllProducts 14 | queryProduct.GetProductByName 15 | commandProduct.UpdateProductName 16 | commandProduct.CreateProduct 17 | commandOrder.CreateOrder 18 | commandOrder.DeleteOrder 19 | queryOrder.GetAllOrders 20 | queryOrder.GetAllOrdersByUserID 21 | queryOrder.GetOrderByID 22 | } 23 | -------------------------------------------------------------------------------- /order/internal/infrastructure/di/factories/messageBroker/message_broker.go: -------------------------------------------------------------------------------- 1 | package messagebrokerconfig 2 | 3 | import ( 4 | messagebroker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker" 5 | brokerconfigurate "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate/order" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/controller" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | var Module = fx.Provide( 12 | messagebroker.BuildAMPQ, 13 | brokerconfigurate.NewMessageBrokerConfigure, 14 | order.NewSetupBroker, 15 | brokerconfigurate.NewBrokerSetup, 16 | controller.NewMessageBroker, 17 | ) 18 | -------------------------------------------------------------------------------- /order/internal/infrastructure/logger/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type LoggerConfig struct { 4 | Mode string `toml:"mode"` 5 | LogOutput string `toml:"log_output"` 6 | LogLevel string `toml:"log_level"` 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/infrastructure/logger/gin.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "go.uber.org/zap" 4 | 5 | type GinLogger struct { 6 | *Logger 7 | } 8 | 9 | func (l GinLogger) Write(p []byte) (n int, err error) { 10 | l.Info(string(p)) 11 | return len(p), nil 12 | } 13 | 14 | func (l Logger) GetGinLogger() GinLogger { 15 | logger := zapLogger.WithOptions( 16 | zap.WithCaller(false), 17 | ) 18 | return GinLogger{ 19 | Logger: newSugaredLogger(logger), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /order/internal/infrastructure/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger/config" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | ) 8 | 9 | type Logger struct { 10 | *zap.SugaredLogger 11 | } 12 | 13 | var ( 14 | globalLogger *Logger 15 | zapLogger *zap.Logger 16 | ) 17 | 18 | func NewLogger(loggerConfig config.LoggerConfig) Logger { 19 | if globalLogger == nil { 20 | logger := newLogger(loggerConfig) 21 | globalLogger = &logger 22 | } 23 | return *globalLogger 24 | } 25 | 26 | func newLogger(loggerConfig config.LoggerConfig) Logger { 27 | var zapConfig zap.Config 28 | if loggerConfig.Mode == "production" { 29 | zapConfig = zap.NewProductionConfig() 30 | } else if loggerConfig.Mode == "development" { 31 | zapConfig = zap.NewDevelopmentConfig() 32 | zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 33 | } 34 | if loggerConfig.Mode == "production" && loggerConfig.LogOutput != "" { 35 | zapConfig.OutputPaths = []string{loggerConfig.LogOutput} 36 | } 37 | 38 | logLevel := loggerConfig.LogLevel 39 | var level zapcore.Level 40 | switch logLevel { 41 | case "debug": 42 | level = zapcore.DebugLevel 43 | case "info": 44 | level = zapcore.InfoLevel 45 | case "warn": 46 | level = zapcore.WarnLevel 47 | case "error": 48 | level = zapcore.ErrorLevel 49 | case "fatal": 50 | level = zapcore.FatalLevel 51 | default: 52 | level = zap.PanicLevel 53 | } 54 | zapConfig.Level.SetLevel(level) 55 | 56 | zapLogger, _ = zapConfig.Build() 57 | logger := newSugaredLogger(zapLogger) 58 | 59 | return *logger 60 | } 61 | func newSugaredLogger(logger *zap.Logger) *Logger { 62 | return &Logger{ 63 | SugaredLogger: logger.Sugar(), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /order/internal/infrastructure/mediator/dispatchers/commandDispatcher/command_dispatcher.go: -------------------------------------------------------------------------------- 1 | package commanddispatcher 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type CommandDispatcher interface { 8 | Send(command interface{}) (interface{}, error) 9 | RegisterCommandHandler(command interface{}, handler CommandHandler) 10 | } 11 | 12 | type CommandDispatcherImpl struct { 13 | CommandHandlers map[reflect.Type]CommandHandler 14 | } 15 | type CommandHandler interface { 16 | Handle(command interface{}) (interface{}, error) 17 | } 18 | 19 | func (c *CommandDispatcherImpl) Send(command interface{}) (interface{}, error) { 20 | var handler CommandHandler 21 | c.GetCommandHandler(command, &handler) 22 | return handler.Handle(command) 23 | } 24 | func (c *CommandDispatcherImpl) GetCommandHandler(command interface{}, receiver interface{}) { 25 | reflect.ValueOf(receiver).Elem().Set(reflect.ValueOf(c.CommandHandlers[reflect.TypeOf(command)])) 26 | } 27 | 28 | func (c *CommandDispatcherImpl) RegisterCommandHandler(command interface{}, handler CommandHandler) { 29 | c.CommandHandlers[reflect.ValueOf(command).Type()] = handler 30 | } 31 | -------------------------------------------------------------------------------- /order/internal/infrastructure/mediator/dispatchers/queryDispatcher/query_dispatcher.go: -------------------------------------------------------------------------------- 1 | package querydispatcher 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/query" 5 | "reflect" 6 | ) 7 | 8 | type QueryDispatcher interface { 9 | Query(query interface{}) (interface{}, error) 10 | RegisterQueryHandler(query interface{}, handler QueryHandler) 11 | } 12 | 13 | type QueryDispatcherImpl struct { 14 | QueryHandlers map[reflect.Type]QueryHandler 15 | } 16 | type QueryHandler interface { 17 | Handle(query interface{}) (interface{}, error) 18 | } 19 | 20 | func (c *QueryDispatcherImpl) Query(query interface{}) (interface{}, error) { 21 | var handler QueryHandler 22 | c.GetQueryHandler(query, &handler) 23 | return handler.Handle(query) 24 | } 25 | func (c *QueryDispatcherImpl) GetQueryHandler(query interface{}, receiver interface{}) { 26 | reflect.ValueOf(receiver).Elem().Set(reflect.ValueOf(c.QueryHandlers[reflect.TypeOf(query)])) 27 | } 28 | 29 | type CreateProductQueryHandler struct { 30 | query.GetAllProducts 31 | } 32 | 33 | func (c *QueryDispatcherImpl) RegisterQueryHandler(query interface{}, handler QueryHandler) { 34 | c.QueryHandlers[reflect.ValueOf(query).Type()] = handler 35 | } 36 | func (c *CreateProductQueryHandler) Query(q interface{}) (interface{}, error) { 37 | return c.GetAllProducts.Get(q.(query.GetAllProductsQuery)) 38 | } 39 | -------------------------------------------------------------------------------- /order/internal/infrastructure/mediator/mediator.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | import ( 4 | c "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/mediator/dispatchers/commandDispatcher" 5 | q "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/mediator/dispatchers/queryDispatcher" 6 | "reflect" 7 | ) 8 | 9 | type Mediator interface { 10 | Send(command interface{}) (interface{}, error) 11 | Query(query interface{}) (interface{}, error) 12 | RegisterCommandHandler(command interface{}, handler c.CommandHandler) 13 | RegisterQueryHandler(command interface{}, handler q.QueryHandler) 14 | } 15 | 16 | type MediatorImpl struct { 17 | queryDispatcher q.QueryDispatcher 18 | commandDispatcher c.CommandDispatcher 19 | } 20 | 21 | func (mediator *MediatorImpl) RegisterCommandHandler(command interface{}, handler c.CommandHandler) { 22 | mediator.commandDispatcher.RegisterCommandHandler(command, handler) 23 | } 24 | 25 | func (mediator *MediatorImpl) RegisterQueryHandler(command interface{}, handler q.QueryHandler) { 26 | mediator.queryDispatcher.RegisterQueryHandler(command, handler) 27 | } 28 | 29 | func (mediator *MediatorImpl) Send(command interface{}) (interface{}, error) { 30 | return mediator.commandDispatcher.Send(command) 31 | } 32 | 33 | func (mediator *MediatorImpl) Query(query interface{}) (interface{}, error) { 34 | return mediator.queryDispatcher.Query(query) 35 | } 36 | 37 | func (MediatorImpl) Create() Mediator { 38 | var commandDispatcher c.CommandDispatcherImpl 39 | var queryDispatcher q.QueryDispatcherImpl 40 | queryDispatcher.QueryHandlers = make(map[reflect.Type]q.QueryHandler) 41 | commandDispatcher.CommandHandlers = make(map[reflect.Type]c.CommandHandler) 42 | return &MediatorImpl{queryDispatcher: &queryDispatcher, commandDispatcher: &commandDispatcher} 43 | } 44 | -------------------------------------------------------------------------------- /order/internal/infrastructure/mediator/orders.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/command" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/query" 6 | ) 7 | 8 | type CreateOrderCommandHandler struct { 9 | command.CreateOrder 10 | } 11 | 12 | func (c *CreateOrderCommandHandler) Handle(cmd interface{}) (interface{}, error) { 13 | return nil, c.Create(cmd.(command.CreateOrderCommand)) 14 | } 15 | 16 | type DeleteOrderCommandHandler struct { 17 | command.DeleteOrder 18 | } 19 | 20 | func (c *DeleteOrderCommandHandler) Handle(cmd interface{}) (interface{}, error) { 21 | return nil, c.Delete(cmd.(command.DeleteOrderCommand)) 22 | } 23 | 24 | type GetAllOrdersQueryHandler struct { 25 | query.GetAllOrders 26 | } 27 | 28 | func (c *GetAllOrdersQueryHandler) Handle(q interface{}) (interface{}, error) { 29 | return c.Get(q.(query.GetAllOrderQuery)) 30 | } 31 | 32 | type GetAllOrdersByUserIDQueryHandler struct { 33 | query.GetAllOrdersByUserID 34 | } 35 | 36 | func (c *GetAllOrdersByUserIDQueryHandler) Handle(q interface{}) (interface{}, error) { 37 | return c.Get(q.(query.GetAllOrderByUserIDQuery)) 38 | } 39 | 40 | type GetOrdersByIDHandler struct { 41 | query.GetOrderByID 42 | } 43 | 44 | func (c *GetOrdersByIDHandler) Handle(q interface{}) (interface{}, error) { 45 | return c.Get(q.(query.GetOrderByIDQuery)) 46 | } 47 | -------------------------------------------------------------------------------- /order/internal/infrastructure/mediator/product.go: -------------------------------------------------------------------------------- 1 | package mediator 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/command" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/interfaces/query" 6 | ) 7 | 8 | type CreateProductCommandHandler struct { 9 | command.CreateProduct 10 | } 11 | 12 | func (c *CreateProductCommandHandler) Handle(cmd interface{}) (interface{}, error) { 13 | return nil, c.Create(cmd.(command.CreateProductCommand)) 14 | } 15 | 16 | type UpdateProductCommandHandler struct { 17 | command.UpdateProductName 18 | } 19 | 20 | func (c *UpdateProductCommandHandler) Handle(cmd interface{}) (interface{}, error) { 21 | return nil, c.Update(cmd.(command.UpdateProductNameCommand)) 22 | } 23 | 24 | type GetAllProductsQueryHandler struct { 25 | query.GetAllProducts 26 | } 27 | 28 | func (c *GetAllProductsQueryHandler) Handle(q interface{}) (interface{}, error) { 29 | return c.GetAllProducts.Get(q.(query.GetAllProductsQuery)) 30 | } 31 | 32 | type GetProductByNameQueryHandler struct { 33 | query.GetProductByName 34 | } 35 | 36 | func (c *GetProductByNameQueryHandler) Handle(q interface{}) (interface{}, error) { 37 | return c.GetProductByName.Get(q.(query.GetProductByNameQuery)) 38 | } 39 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/brokerConfigurate/broker_configurate.go: -------------------------------------------------------------------------------- 1 | package brokerconfigurate 2 | 3 | import ( 4 | messagebroker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate/interfaces" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit" 7 | ) 8 | 9 | type MessageBrokerConfigure struct { 10 | interfaces.BaseMessageBrokerConfigure 11 | Channel *rabbit.ReusableChannel 12 | } 13 | 14 | func (m *MessageBrokerConfigure) DeclareExchange(exchangeName string) { 15 | err := m.Channel.ExchangeDeclare(exchangeName, "topic", true, false, false, false, nil) 16 | if err != nil { 17 | panic(err) 18 | } 19 | } 20 | func (m *MessageBrokerConfigure) DeclareQueue(queueName string) { 21 | _, err := m.Channel.QueueDeclare(queueName, true, false, false, false, nil) 22 | if err != nil { 23 | panic(err) 24 | } 25 | } 26 | func (m *MessageBrokerConfigure) BindExchangeQueue(exchangeName, key, queueName string) { 27 | err := m.Channel.QueueBind(queueName, key, exchangeName, false, nil) 28 | if err != nil { 29 | panic(err) 30 | } 31 | } 32 | func NewMessageBrokerConfigure(rabbit messagebroker.Rabbit) interfaces.BaseMessageBrokerConfigure { 33 | return &MessageBrokerConfigure{ 34 | Channel: rabbit.GetChannel(), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/brokerConfigurate/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | type BaseMessageBrokerConfigure interface { 4 | DeclareExchange(exchangeName string) 5 | DeclareQueue(queueName string) 6 | BindExchangeQueue(exchangeName, key, queueName string) 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/brokerConfigurate/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate/interfaces" 5 | ) 6 | 7 | var ExchangeName = "Orders" 8 | var QueueName = "Orders" 9 | 10 | type BrokerSetup struct { 11 | interfaces.BaseMessageBrokerConfigure 12 | } 13 | 14 | func (b BrokerSetup) Setup() { 15 | b.DeclareExchange(ExchangeName) 16 | 17 | b.DeclareQueue(QueueName) 18 | b.BindExchangeQueue(ExchangeName, "Order.*", QueueName) 19 | 20 | // Optional queue build only for monolith structure 21 | b.DeclareQueue("CustomerSaga") 22 | b.BindExchangeQueue(ExchangeName, "Customer.Saga.*", "CustomerSaga") 23 | b.DeclareQueue("OrderSaga") 24 | b.BindExchangeQueue(ExchangeName, "Order.Saga.*", "") 25 | } 26 | func NewSetupBroker(broker interfaces.BaseMessageBrokerConfigure) BrokerSetup { 27 | return BrokerSetup{ 28 | BaseMessageBrokerConfigure: broker, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/brokerConfigurate/setup.go: -------------------------------------------------------------------------------- 1 | package brokerconfigurate 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate/order" 5 | ) 6 | 7 | type Brokers []Broker 8 | type Broker interface { 9 | Setup() 10 | } 11 | 12 | func NewBrokerSetup( 13 | orderSetup order.BrokerSetup, 14 | ) Brokers { 15 | return Brokers{ 16 | orderSetup, 17 | } 18 | } 19 | func (r Brokers) Setup() { 20 | for _, broker := range r { 21 | broker.Setup() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type MessageBrokerConfig struct { 6 | Host string `toml:"host"` 7 | Port int `toml:"port"` 8 | Login string `toml:"login"` 9 | Password string `toml:"password"` 10 | MaxChannels int `toml:"max_channels"` 11 | } 12 | 13 | func (conf *MessageBrokerConfig) FullDNS() string { 14 | return fmt.Sprintf( 15 | "amqp://%s:%s@%s:%d/", 16 | conf.Login, conf.Password, conf.Host, conf.Port, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/common/interfaces/broker" 6 | messagebroker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit" 8 | "github.com/rabbitmq/amqp091-go" 9 | ) 10 | 11 | type MessageBrokerImpl struct { 12 | broker.MessageBroker 13 | *rabbit.ReusableChannel 14 | } 15 | 16 | func (m *MessageBrokerImpl) PublishMessage(ctx context.Context, exchangeName, routingKey string, message []byte) error { 17 | err := m.ReusableChannel.PublishWithContext( 18 | ctx, 19 | exchangeName, 20 | routingKey, 21 | true, 22 | false, 23 | m.BuildMessage(message), 24 | ) 25 | return err 26 | } 27 | func (m *MessageBrokerImpl) BuildMessage(message []byte) amqp091.Publishing { 28 | return amqp091.Publishing{ 29 | ContentType: "application/json", 30 | Body: message, 31 | } 32 | } 33 | func NewMessageBroker(rabbit messagebroker.Rabbit) broker.MessageBroker { 34 | return &MessageBrokerImpl{ 35 | ReusableChannel: rabbit.GetChannel(), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /order/internal/infrastructure/messageBroker/message_broker.go: -------------------------------------------------------------------------------- 1 | package messagebroker 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/config" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit" 7 | ) 8 | 9 | type Rabbit struct { 10 | *rabbit.Pool 11 | } 12 | 13 | func BuildAMPQ(config config.MessageBrokerConfig, logger logger.Logger) Rabbit { 14 | pool, err := rabbit.NewPool(config.FullDNS(), config.MaxChannels, logger) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return Rabbit{Pool: pool} 19 | } 20 | -------------------------------------------------------------------------------- /order/internal/presentation/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/config" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/engine" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/prometheus" 11 | api "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/api" 12 | "go.uber.org/fx" 13 | ) 14 | 15 | var Module = fx.Options( 16 | api.Module, 17 | fx.Invoke(Start), 18 | ) 19 | 20 | func Start( 21 | lifecycle fx.Lifecycle, 22 | router engine.RequestHandler, 23 | config config.APIConfig, 24 | logger logger.Logger, 25 | routers routes.Routes, //nolint:all 26 | prometheus prometheus.Prometheus, 27 | ) { 28 | routers.Setup() 29 | prometheus.Use(router.Gin) 30 | 31 | lifecycle.Append( 32 | fx.Hook{ 33 | OnStart: func(context.Context) error { 34 | logger.Info(fmt.Sprintf("Starting application in :%d", config.Port)) 35 | go func() { 36 | defer func() { 37 | if r := recover(); r != nil { 38 | logger.Info(fmt.Sprintf("Recovered when boot api server, r %s", r)) 39 | } 40 | }() 41 | err := router.Gin.Run(fmt.Sprintf("%s:%d", config.Host, config.Port)) 42 | if err != nil { 43 | panic(err) 44 | } 45 | }() 46 | return nil 47 | }, 48 | OnStop: func(context.Context) error { 49 | logger.Info("Stopping api application") 50 | return nil 51 | }, 52 | }, 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /order/internal/presentation/api/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type APIConfig struct { 4 | Host string 5 | Port int 6 | BaseURLPrefix string `toml:"base_url_prefix"` 7 | Mode string 8 | } 9 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/handlers/healthcheck/handler.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | type OkMessage struct { 9 | Status string `json:"status"` 10 | } 11 | type Handler struct { 12 | } 13 | 14 | func (c *Handler) GetStatus(context *gin.Context) { 15 | context.JSON(http.StatusOK, OkMessage{Status: "OK"}) 16 | } 17 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/handlers/healthcheck/healthcheck.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | func NewHealthCheckHandler() Handler { 4 | return Handler{} 5 | } 6 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/handlers/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/mediator" 5 | ) 6 | 7 | func NewOrderHandler( 8 | mediator mediator.Mediator, 9 | ) Handler { 10 | return Handler{ 11 | mediator: mediator, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/handlers/params.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "strconv" 6 | ) 7 | 8 | type ( 9 | Limit int 10 | Offset int 11 | Order string 12 | ) 13 | 14 | func GetQueryParams(context *gin.Context) (Limit, Offset, Order) { 15 | limit, _ := strconv.Atoi(context.DefaultQuery("limit", "1000")) 16 | offset, _ := strconv.Atoi(context.DefaultQuery("offset", "0")) 17 | order := context.DefaultQuery("order", "asc") 18 | return Limit(limit), Offset(offset), Order(order) 19 | } 20 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/handlers/product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/mediator" 5 | ) 6 | 7 | func NewProductHandler( 8 | mediator mediator.Mediator, 9 | ) Handler { 10 | return Handler{ 11 | mediator: mediator, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/response/exceptions.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/common" 5 | ) 6 | 7 | type ExceptionResponse struct { 8 | Message string `json:"message"` 9 | Data string `json:"data"` 10 | } 11 | 12 | func SetExceptionPayload(response *ExceptionResponse, exc common.CustomException) { 13 | response.Message = exc.Message 14 | response.Data = exc.Ctx 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/routes/group.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | c "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/config" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/engine" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware" 7 | ) 8 | 9 | func NewGroupRoutes(config c.APIConfig, handler engine.RequestHandler, middlewares middleware.Middlewares) engine.GroupRoutes { 10 | return engine.GroupRoutes{RouterGroup: handler.Gin.Group(config.BaseURLPrefix, middlewares...)} 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/routes/healthcheck/healthcheck.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/handlers/healthcheck" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/engine" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/logging" 7 | ) 8 | 9 | type Routes struct { 10 | controller healthcheck.Handler 11 | logging.LoggerMiddleware 12 | engine.RequestHandler 13 | } 14 | 15 | func (r Routes) Setup() { 16 | r.RequestHandler.Gin.GET("healthcheck/", r.controller.GetStatus, r.LoggerMiddleware.Handle) 17 | } 18 | 19 | func NewHealthCheckRoutes( 20 | handler engine.RequestHandler, 21 | controller healthcheck.Handler, 22 | logging logging.LoggerMiddleware, 23 | ) Routes { 24 | return Routes{controller: controller, RequestHandler: handler, LoggerMiddleware: logging} 25 | } 26 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/routes/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/handlers/order" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/engine" 6 | ) 7 | 8 | type Routes struct { 9 | engine.GroupRoutes 10 | controller order.Handler 11 | } 12 | 13 | func (r Routes) Setup() { 14 | r.POST("/orders", r.controller.CreateOrder) 15 | r.DELETE("/orders", r.controller.DeleteOrder) 16 | 17 | r.GET("/orders", r.controller.GetAllOrders) 18 | r.GET("/orders/:id", r.controller.GetOrderByID) 19 | r.GET("/orders/user/:userID", r.controller.GetAllOrdersByUserID) 20 | } 21 | 22 | func NewOrderRoutes( 23 | group engine.GroupRoutes, 24 | controller order.Handler, 25 | ) Routes { 26 | return Routes{controller: controller, GroupRoutes: group} 27 | } 28 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/routes/product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/handlers/product" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/engine" 6 | ) 7 | 8 | type Routes struct { 9 | engine.GroupRoutes 10 | controller product.Handler 11 | } 12 | 13 | func (r Routes) Setup() { 14 | r.POST("/products", r.controller.CreateProduct) 15 | r.PUT("/products/:productID/productName", r.controller.UpdateProductName) 16 | r.GET("/products", r.controller.GetAllProducts) 17 | r.GET("/products/:productName", r.controller.GetProductByName) 18 | } 19 | 20 | func NewProductRoutes( 21 | group engine.GroupRoutes, 22 | controller product.Handler, 23 | ) Routes { 24 | return Routes{controller: controller, GroupRoutes: group} 25 | } 26 | -------------------------------------------------------------------------------- /order/internal/presentation/api/controllers/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes/healthcheck" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes/order" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes/product" 7 | ) 8 | 9 | type Routes []Route 10 | 11 | type Route interface { 12 | Setup() 13 | } 14 | 15 | func NewRoutes( 16 | productRoutes product.Routes, 17 | orderRoutes order.Routes, 18 | healthcheckRoutes healthcheck.Routes, 19 | ) Routes { 20 | return Routes{ 21 | productRoutes, 22 | orderRoutes, 23 | healthcheckRoutes, 24 | } 25 | } 26 | func (r Routes) Setup() { 27 | for _, route := range r { 28 | route.Setup() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /order/internal/presentation/api/engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 5 | api "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/config" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type RequestHandler struct { 10 | Gin *gin.Engine 11 | } 12 | type GroupRoutes struct { 13 | *gin.RouterGroup 14 | } 15 | 16 | func NewRequestHandler(logger logger.Logger, config api.APIConfig) RequestHandler { 17 | gin.DefaultWriter = logger.GetGinLogger() 18 | if config.Mode == "production" { 19 | gin.SetMode(gin.ReleaseMode) 20 | } 21 | return RequestHandler{Gin: gin.New()} 22 | } 23 | -------------------------------------------------------------------------------- /order/internal/presentation/api/middleware/errorHandler/errro_handling.go: -------------------------------------------------------------------------------- 1 | package errorhandler 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/response" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/interfaces" 8 | "github.com/gin-gonic/gin" 9 | "net/http" 10 | ) 11 | 12 | type ErrorMiddleware struct { 13 | interfaces.Middleware 14 | logger.Logger 15 | } 16 | type ErrorCatching struct { 17 | status *int 18 | err error 19 | exception *response.ExceptionResponse 20 | } 21 | type ErrorStatus struct { 22 | status int 23 | exception any 24 | } 25 | 26 | func NewErrorMiddleware(logger logger.Logger) ErrorMiddleware { 27 | return ErrorMiddleware{ 28 | Logger: logger, 29 | } 30 | } 31 | 32 | func (m ErrorMiddleware) Handle(c *gin.Context) { 33 | c.Next() 34 | for _, err := range c.Errors { 35 | status, exceptionResponse := http.StatusInternalServerError, response.ExceptionResponse{ 36 | Message: "Unknown server error has occurred", 37 | Data: err.Error(), 38 | } 39 | errorCatching := ErrorCatching{status: &status, err: err, exception: &exceptionResponse} 40 | 41 | handleProductError(errorCatching) 42 | handleOrderError(errorCatching) 43 | m.Logger.Info( 44 | fmt.Sprintf("Server handle erorr with status: %d, and error message: %s", 45 | *errorCatching.status, errorCatching.err.Error()), 46 | ) 47 | c.JSON(*errorCatching.status, *errorCatching.exception) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /order/internal/presentation/api/middleware/errorHandler/order.go: -------------------------------------------------------------------------------- 1 | package errorhandler 2 | 3 | import ( 4 | "errors" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/exceptions" 6 | domain "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/order/exceptions" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/response" 8 | "net/http" 9 | ) 10 | 11 | func handleOrderError(e ErrorCatching) { 12 | var orderProductsError *domain.OrderProductsEmpty 13 | var orderIDError *exceptions.OrderIDNotExist 14 | var orderProductsIDsError *exceptions.ProductIDsNotExist 15 | 16 | if errors.As(e.err, &orderProductsError) { 17 | *e.status = http.StatusBadRequest 18 | response.SetExceptionPayload(e.exception, orderProductsError.CustomException) 19 | } 20 | if errors.As(e.err, &orderIDError) { 21 | *e.status = http.StatusNotFound 22 | response.SetExceptionPayload(e.exception, orderIDError.CustomException) 23 | } 24 | if errors.As(e.err, &orderProductsIDsError) { 25 | *e.status = http.StatusNotFound 26 | response.SetExceptionPayload(e.exception, orderProductsIDsError.CustomException) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /order/internal/presentation/api/middleware/errorHandler/product.go: -------------------------------------------------------------------------------- 1 | package errorhandler 2 | 3 | import ( 4 | "errors" 5 | application "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/product/exceptions" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/domain/product/exceptions" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/response" 8 | "net/http" 9 | ) 10 | 11 | func handleProductError(e ErrorCatching) { 12 | var discountError *exceptions.InvalidDiscountProductCreation 13 | var priceError *exceptions.InvalidPriceProductCreation 14 | var incorrectProductName *exceptions.InvalidProductNameUpdate 15 | var productNameError *application.ProductNameNotExist 16 | var productIDError *application.ProductIDNotExist 17 | 18 | if errors.As(e.err, &discountError) { 19 | *e.status = http.StatusBadRequest 20 | response.SetExceptionPayload(e.exception, discountError.CustomException) 21 | } 22 | if errors.As(e.err, &priceError) { 23 | *e.status = http.StatusBadRequest 24 | response.SetExceptionPayload(e.exception, priceError.CustomException) 25 | } 26 | if errors.As(e.err, &productNameError) { 27 | *e.status = http.StatusNotFound 28 | response.SetExceptionPayload(e.exception, productNameError.CustomException) 29 | } 30 | if errors.As(e.err, &productIDError) { 31 | *e.status = http.StatusNotFound 32 | response.SetExceptionPayload(e.exception, productIDError.CustomException) 33 | } 34 | if errors.As(e.err, &incorrectProductName) { 35 | *e.status = http.StatusBadRequest 36 | response.SetExceptionPayload(e.exception, incorrectProductName.CustomException) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /order/internal/presentation/api/middleware/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type Middleware interface { 6 | Handle(c *gin.Context) 7 | } 8 | -------------------------------------------------------------------------------- /order/internal/presentation/api/middleware/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/interfaces" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type LoggerMiddleware struct { 10 | logger.Logger 11 | interfaces.Middleware 12 | } 13 | 14 | func NewLoggerMiddleware(logger logger.Logger) LoggerMiddleware { 15 | return LoggerMiddleware{ 16 | Logger: logger, 17 | } 18 | } 19 | func (m LoggerMiddleware) Handle(c *gin.Context) { 20 | c.Next() 21 | m.Logger.Infow( 22 | "Api good answer Request", 23 | "Request type", c.Request.Method, 24 | "Request status", c.Writer.Status(), 25 | "Content length", c.Writer.Size(), 26 | "Request path", c.Request.URL, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /order/internal/presentation/api/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/errorHandler" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/logging" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type Middlewares []gin.HandlerFunc 10 | 11 | func NewMiddlewares( 12 | errorMiddleware errorhandler.ErrorMiddleware, 13 | loggingMiddleware logging.LoggerMiddleware, 14 | ) Middlewares { 15 | return Middlewares{ 16 | errorMiddleware.Handle, 17 | loggingMiddleware.Handle, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /order/internal/presentation/api/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ginprometheus "github.com/zsais/go-gin-prometheus" 4 | 5 | type Prometheus struct { 6 | *ginprometheus.Prometheus 7 | } 8 | 9 | func NewPrometheus() Prometheus { 10 | return Prometheus{Prometheus: ginprometheus.NewPrometheus("gin")} 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/presentation/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | cache "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/config" 5 | db "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/config" 6 | logger "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger/config" 7 | broker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/config" 8 | api "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/config" 9 | cron "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/config" 10 | ) 11 | 12 | type AppConfig struct { 13 | Mode string 14 | } 15 | type Config struct { 16 | AppConfig `toml:"app"` 17 | db.DBConfig `toml:"db"` 18 | api.APIConfig `toml:"api"` 19 | broker.MessageBrokerConfig `toml:"broker"` 20 | cron.CronConfig `toml:"handlers"` 21 | logger.LoggerConfig `toml:"logging"` 22 | cache.RedisConfig `toml:"cache"` 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/presentation/config/factories.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | cache "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/cache/config" 5 | load "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/config" 6 | db "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/db/config" 7 | logger "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger/config" 8 | broker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/config" 9 | api "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/config" 10 | cron "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/config" 11 | ) 12 | 13 | func NewConfig() Config { 14 | var conf Config 15 | load.LoadConfig(&conf, "", "") 16 | return conf 17 | } 18 | func NewDBConfig(config Config) db.DBConfig { 19 | return config.DBConfig 20 | } 21 | func NewAppConfig(config Config) AppConfig { 22 | return config.AppConfig 23 | } 24 | func NewAPIConfig(config Config) api.APIConfig { 25 | config.APIConfig.Mode = config.AppConfig.Mode 26 | return config.APIConfig 27 | } 28 | func NewBrokerConfig(config Config) broker.MessageBrokerConfig { 29 | return config.MessageBrokerConfig 30 | } 31 | func NewCronConfig(config Config) cron.CronConfig { 32 | return config.CronConfig 33 | } 34 | func NewLoggerConfig(config Config) logger.LoggerConfig { 35 | config.LoggerConfig.Mode = config.AppConfig.Mode 36 | return config.LoggerConfig 37 | } 38 | func NewCacheConfig(config Config) cache.RedisConfig { 39 | return config.RedisConfig 40 | } 41 | -------------------------------------------------------------------------------- /order/internal/presentation/consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 7 | brokerconfigurate "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/consumer/subscribers" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/consumer" 10 | "go.uber.org/fx" 11 | ) 12 | 13 | var Module = fx.Options( 14 | consumer.Module, 15 | fx.Invoke(Start), 16 | ) 17 | 18 | func Start( 19 | lifecycle fx.Lifecycle, 20 | logger logger.Logger, 21 | consumer subscribers.Subscribers, //nolint:all 22 | setup brokerconfigurate.Brokers, //nolint:all 23 | ) { 24 | setup.Setup() 25 | lifecycle.Append( 26 | fx.Hook{ 27 | OnStart: func(ctx context.Context) error { 28 | go func() { 29 | defer func() { 30 | if r := recover(); r != nil { 31 | logger.Info(fmt.Sprintf("Recovered when boot consumer, r %s", r)) 32 | } 33 | consumer.Listen() 34 | }() 35 | }() 36 | return nil 37 | }, 38 | OnStop: func(context.Context) error { 39 | logger.Info("Stopping consumer application") 40 | return nil 41 | }, 42 | }, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /order/internal/presentation/consumer/subscribers/order/events.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/cache" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit" 8 | ) 9 | 10 | type OrderEvent struct { 11 | *rabbit.ReusableChannel 12 | logger.Logger 13 | cache.OrderCache 14 | } 15 | 16 | func (s OrderEvent) Listen() { 17 | defer s.ReusableChannel.Release() 18 | // TODO: ADD ACK FOR EVENTS 19 | messages, _ := s.ReusableChannel.Consume( 20 | "Orders", 21 | "", 22 | true, 23 | false, 24 | false, 25 | false, 26 | nil, 27 | ) 28 | 29 | var orderAddProduct cache.OrderAddProductSubscribe 30 | var orderCreate cache.OrderCreateSubscribe 31 | var orderDelete cache.OrderDeleteEvent 32 | var eventType cache.EventType 33 | var str string 34 | go func() { 35 | for message := range messages { 36 | _ = json.Unmarshal(message.Body, &str) 37 | err := json.Unmarshal([]byte(str), &eventType) 38 | if err != nil { 39 | s.Info("Error unmarshal event type; err %s", err.Error()) 40 | } 41 | switch eventType.EventType { 42 | case "OrderCreated": 43 | err = json.Unmarshal([]byte(str), &orderCreate) 44 | if err != nil { 45 | continue 46 | } 47 | s.OrderCache.OrderCreateEvent(orderCreate) 48 | case "OrderAddProduct": 49 | err = json.Unmarshal([]byte(str), &orderAddProduct) 50 | if err != nil { 51 | continue 52 | } 53 | s.OrderCache.OrderAddProductEvent(orderAddProduct) 54 | case "OrderDeleted": 55 | err = json.Unmarshal([]byte(str), &orderDelete) 56 | if err != nil { 57 | continue 58 | } 59 | s.OrderCache.OrderDeleteEvent(orderDelete) 60 | } 61 | } 62 | }() 63 | } 64 | -------------------------------------------------------------------------------- /order/internal/presentation/consumer/subscribers/order/saga.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/saga" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit" 8 | ) 9 | 10 | type SagaCreateSubscriber struct { 11 | *rabbit.ReusableChannel 12 | logger.Logger 13 | saga.CreateOrder 14 | } 15 | 16 | func (s SagaCreateSubscriber) Listen() { 17 | defer s.ReusableChannel.Release() 18 | // TODO: ADD ACK FOR SAGA 19 | 20 | messages, _ := s.ReusableChannel.Consume( 21 | "CustomerSaga", 22 | "", 23 | true, 24 | false, 25 | false, 26 | false, 27 | nil, 28 | ) 29 | var m saga.Message 30 | go func() { 31 | for message := range messages { 32 | err := json.Unmarshal(message.Body, &m) 33 | if err != nil { 34 | s.Info("Error unmarshal event type; err %s", err.Error()) 35 | } 36 | s.CreateOrder.CheckStatus(m) 37 | } 38 | }() 39 | } 40 | -------------------------------------------------------------------------------- /order/internal/presentation/consumer/subscribers/subscribers.go: -------------------------------------------------------------------------------- 1 | package subscribers 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/cache" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/order/interfaces/saga" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 7 | messagebroker "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/consumer/subscribers/order" 9 | ) 10 | 11 | func NewEventConsumer( 12 | rabbit messagebroker.Rabbit, 13 | sagaOrderCreate saga.CreateOrder, 14 | cache cache.OrderCache, 15 | logger logger.Logger, 16 | ) Subscribers { 17 | return Subscribers{ 18 | order.SagaCreateSubscriber{ReusableChannel: rabbit.GetChannel(), CreateOrder: sagaOrderCreate, Logger: logger}, 19 | order.OrderEvent{ReusableChannel: rabbit.GetChannel(), Logger: logger, OrderCache: cache}, 20 | } 21 | } 22 | 23 | type Subscriber interface { 24 | Listen() 25 | } 26 | type Subscribers []Subscriber 27 | 28 | func (w Subscribers) Listen() { 29 | for _, worker := range w { 30 | go worker.Listen() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /order/internal/presentation/cron/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type CronConfig struct { 4 | Seconds int `toml:"seconds"` 5 | } 6 | -------------------------------------------------------------------------------- /order/internal/presentation/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/logger" 7 | brokerconfigurate "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/infrastructure/messageBroker/brokerConfigurate" 8 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/engine" 9 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/handlers" 10 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/cron" 11 | "go.uber.org/fx" 12 | ) 13 | 14 | var Module = fx.Options( 15 | cron.Module, 16 | fx.Invoke(Start), 17 | ) 18 | 19 | func Start( 20 | lifecycle fx.Lifecycle, 21 | setup brokerconfigurate.Brokers, //nolint:all 22 | handlers handlers.Handlers, //nolint:all 23 | cron engine.CronController, //nolint:all 24 | logger logger.Logger, 25 | ) { 26 | setup.Setup() 27 | handlers.Setup() 28 | 29 | lifecycle.Append( 30 | fx.Hook{ 31 | OnStart: func(ctx context.Context) error { 32 | go func() { 33 | defer func() { 34 | if r := recover(); r != nil { 35 | logger.Info(fmt.Sprintf("Recovered when boot cron, r %s", r)) 36 | } 37 | }() 38 | cron.Run() 39 | }() 40 | return nil 41 | }, 42 | OnStop: func(context.Context) error { 43 | logger.Info("Stopping cron application") 44 | return nil 45 | }, 46 | }, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /order/internal/presentation/cron/engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/config" 5 | "github.com/robfig/cron/v3" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func NewCron() *cron.Cron { 12 | return cron.New() 13 | } 14 | func NewCronController(cron *cron.Cron, config config.CronConfig) CronController { 15 | return CronController{ 16 | Cron: cron, 17 | Config: config, 18 | } 19 | } 20 | 21 | type CronController struct { 22 | Cron *cron.Cron 23 | Config config.CronConfig 24 | } 25 | 26 | func (c *CronController) Run() { 27 | c.Cron.Run() 28 | kill := make(chan os.Signal, 1) 29 | signal.Notify(kill, syscall.SIGINT, syscall.SIGTERM) 30 | <-kill 31 | } 32 | -------------------------------------------------------------------------------- /order/internal/presentation/cron/handlers/relay/relay.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/application/relay/interfaces/interactors" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/engine" 7 | ) 8 | 9 | func NewCronHandler(controller engine.CronController, relay interactors.Relay) CronRelayHandler { 10 | return CronRelayHandler{ 11 | CronController: &controller, 12 | Relay: relay, 13 | } 14 | } 15 | 16 | type CronRelayHandler struct { 17 | *engine.CronController 18 | interactors.Relay 19 | } 20 | 21 | func (c CronRelayHandler) Setup() { 22 | _, err := c.Cron.AddFunc( 23 | fmt.Sprintf("@every %ds", c.Config.Seconds), 24 | c.Relay.SendMessagesToBroker, 25 | ) 26 | if err != nil { 27 | panic(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /order/internal/presentation/cron/handlers/setup.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/handlers/relay" 4 | 5 | type Handlers []Handler 6 | 7 | type Handler interface { 8 | Setup() 9 | } 10 | 11 | func NewHandlers( 12 | relay relay.CronRelayHandler, 13 | ) Handlers { 14 | return Handlers{ 15 | relay, 16 | } 17 | } 18 | 19 | func (c Handlers) Setup() { 20 | for _, handler := range c { 21 | handler.Setup() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/presentation/di/api/api.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/prometheus" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/api/controllers" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/api/engine" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/api/middleware" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | var Module = fx.Module( 12 | "presentation.api", 13 | fx.Options( 14 | middleware.Module, 15 | engine.Module, 16 | controllers.Module, 17 | fx.Provide( 18 | prometheus.NewPrometheus, 19 | ), 20 | ), 21 | ) 22 | -------------------------------------------------------------------------------- /order/internal/presentation/di/api/controllers/controllers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/api/controllers/handlers" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/di/api/controllers/routes" 6 | "go.uber.org/fx" 7 | ) 8 | 9 | var Module = fx.Options( 10 | handlers.Module, 11 | routes.Module, 12 | ) 13 | -------------------------------------------------------------------------------- /order/internal/presentation/di/api/controllers/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/handlers/healthcheck" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/handlers/order" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/handlers/product" 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Provide( 11 | product.NewProductHandler, 12 | order.NewOrderHandler, 13 | healthcheck.NewHealthCheckHandler, 14 | ) 15 | -------------------------------------------------------------------------------- /order/internal/presentation/di/api/controllers/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes/healthcheck" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes/order" 7 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/controllers/routes/product" 8 | "go.uber.org/fx" 9 | ) 10 | 11 | var Module = fx.Provide( 12 | routes.NewRoutes, 13 | routes.NewGroupRoutes, 14 | 15 | product.NewProductRoutes, 16 | order.NewOrderRoutes, 17 | healthcheck.NewHealthCheckRoutes, 18 | ) 19 | -------------------------------------------------------------------------------- /order/internal/presentation/di/api/engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/engine" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Provide( 9 | engine.NewRequestHandler, 10 | ) 11 | -------------------------------------------------------------------------------- /order/internal/presentation/di/api/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware" 5 | errorhandler "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/errorHandler" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/api/middleware/logging" 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Provide( 11 | errorhandler.NewErrorMiddleware, 12 | logging.NewLoggerMiddleware, 13 | middleware.NewMiddlewares, 14 | ) 15 | -------------------------------------------------------------------------------- /order/internal/presentation/di/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/config" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Module( 9 | "presentation.config", 10 | fx.Provide( 11 | config.NewConfig, 12 | config.NewDBConfig, 13 | config.NewAPIConfig, 14 | config.NewBrokerConfig, 15 | config.NewCronConfig, 16 | config.NewLoggerConfig, 17 | config.NewCacheConfig, 18 | config.NewAppConfig, 19 | ), 20 | ) 21 | -------------------------------------------------------------------------------- /order/internal/presentation/di/consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/consumer/subscribers" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Module( 9 | "presentation.consumer", 10 | fx.Provide( 11 | subscribers.NewEventConsumer, 12 | ), 13 | ) 14 | -------------------------------------------------------------------------------- /order/internal/presentation/di/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/engine" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/handlers" 6 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/cron/handlers/relay" 7 | "go.uber.org/fx" 8 | ) 9 | 10 | var Module = fx.Module( 11 | "presentation.cron", 12 | fx.Provide( 13 | handlers.NewHandlers, 14 | relay.NewCronHandler, 15 | engine.NewCron, 16 | engine.NewCronController, 17 | ), 18 | ) 19 | -------------------------------------------------------------------------------- /order/internal/presentation/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "github.com/MikhailGulkin/CleanGolangOrderApp/order/internal/presentation/config" 5 | "github.com/goccy/go-graphviz" 6 | "go.uber.org/fx" 7 | "os" 8 | ) 9 | 10 | var Module = fx.Options(fx.Invoke(Start)) 11 | 12 | func Start( 13 | config config.AppConfig, 14 | diGraph fx.DotGraph, 15 | ) { 16 | if config.Mode != "development" { 17 | return 18 | } 19 | g := graphviz.New() 20 | graph, _ := graphviz.ParseBytes([]byte(diGraph)) 21 | os.WriteFile("./graph.gv", []byte(diGraph), 0644) 22 | g.RenderFilename(graph, graphviz.PNG, "./graph.png") 23 | g.RenderFilename(graph, graphviz.SVG, "./graph.svg") 24 | } 25 | -------------------------------------------------------------------------------- /pkg/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import "os" 4 | 5 | func GetEnv(key string, defaultVal string) string { 6 | if value, exists := os.LookupEnv(key); exists { 7 | return value 8 | } 9 | 10 | return defaultVal 11 | } 12 | -------------------------------------------------------------------------------- /pkg/env/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MikhailGulkin/CleanGolangOrderApp/pkg/env 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /pkg/rabbit/channel.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import "github.com/rabbitmq/amqp091-go" 4 | 5 | type ReusableChannel struct { 6 | *amqp091.Channel // the Channel amqp 7 | channels chan *amqp091.Channel // a go channel to store the channels 8 | } 9 | 10 | // newChannel create a new channel 11 | func newChannel(connection *amqp091.Connection) (*amqp091.Channel, error) { 12 | channel, err := connection.Channel() 13 | if err != nil { 14 | return nil, err 15 | } 16 | return channel, nil 17 | } 18 | 19 | // newReusableChannel create a new reusable channel 20 | func newReusableChannel(channel *amqp091.Channel, channels chan *amqp091.Channel) *ReusableChannel { 21 | return &ReusableChannel{channels: channels, Channel: channel} 22 | } 23 | func (r *ReusableChannel) Release() { 24 | r.channels <- r.Channel 25 | } 26 | -------------------------------------------------------------------------------- /pkg/rabbit/connection.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import "github.com/rabbitmq/amqp091-go" 4 | 5 | // connect establish the connection with the broker amqp 6 | func connect(connectionString string) (*amqp091.Connection, error) { 7 | connection, err := amqp091.Dial(connectionString) 8 | if err != nil { 9 | return nil, err 10 | } 11 | 12 | return connection, nil 13 | } 14 | -------------------------------------------------------------------------------- /pkg/rabbit/examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type Logger struct { 11 | logger *log.Logger 12 | } 13 | 14 | func (l *Logger) Info(args ...interface{}) { 15 | l.logger.Print(args) 16 | } 17 | 18 | func (l *Logger) Panic(args ...interface{}) { 19 | l.logger.Panic(args) 20 | } 21 | 22 | func main() { 23 | logger := Logger{logger: log.Default()} 24 | 25 | pool, err := rabbit.NewPool("amqp://admin:admin@localhost:5672/", 3, &logger) 26 | defer func(pool *rabbit.Pool) { 27 | err := pool.Close() 28 | if err != nil { 29 | fmt.Print(err) 30 | } 31 | }(pool) 32 | 33 | if err != nil { 34 | fmt.Print(err) 35 | } 36 | for i := 1; i <= 10; i++ { 37 | go func() { 38 | channel := pool.GetChannel() 39 | defer channel.Release() 40 | fmt.Println(channel.Channel) 41 | time.Sleep(3 * time.Second) 42 | }() 43 | 44 | } 45 | 46 | time.Sleep(5 * time.Second) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /pkg/rabbit/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MikhailGulkin/CleanGolangOrderApp/pkg/rabbit 2 | 3 | go 1.20 4 | 5 | require github.com/rabbitmq/amqp091-go v1.8.1 6 | -------------------------------------------------------------------------------- /pkg/rabbit/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 4 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= 8 | github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 14 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /pkg/rabbit/interfaces.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | // Logger is an interface for logging 4 | type logger interface { 5 | Info(args ...interface{}) 6 | } 7 | -------------------------------------------------------------------------------- /pkg/rabbit/pool.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rabbitmq/amqp091-go" 6 | ) 7 | 8 | type Pool struct { 9 | connection *amqp091.Connection //the connection amqp 10 | maxChannels int //the maximum quantity of channels of pool 11 | channels chan *amqp091.Channel //a go channel to store the channels 12 | logger logger // a logger to log information 13 | } 14 | 15 | // NewPool create a new pool of channels 16 | func NewPool(connectionString string, maxChannels int, logger logger) (*Pool, error) { 17 | 18 | connection, err := connect(connectionString) 19 | if err != nil { 20 | return nil, err 21 | } 22 | reusableChannels := make(chan *amqp091.Channel, maxChannels) 23 | 24 | for id := 0; id < maxChannels; id++ { 25 | reusableChannel, err := newChannel(connection) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | reusableChannels <- reusableChannel 31 | } 32 | 33 | logger.Info("Pool created successfully") 34 | return &Pool{ 35 | connection: connection, 36 | maxChannels: maxChannels, 37 | channels: reusableChannels, 38 | logger: logger, 39 | }, nil 40 | } 41 | 42 | // Close the connection with the broker amqp 43 | func (pool *Pool) Close() error { 44 | 45 | connection := pool.connection 46 | 47 | if err := connection.Close(); err != nil { 48 | errMsg := fmt.Sprintf( 49 | "Occurred an error to try close the connection with the amqp broker: %v", 50 | err.Error(), 51 | ) 52 | return fmt.Errorf(errMsg) 53 | } 54 | close(pool.channels) 55 | 56 | pool.logger.Info("Connection with the amqp broker was closed successfully") 57 | return nil 58 | } 59 | 60 | // GetChannel get a channel of the pool to use 61 | func (pool *Pool) GetChannel() *ReusableChannel { 62 | return newReusableChannel(<-pool.channels, pool.channels) 63 | } 64 | --------------------------------------------------------------------------------