├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── Dockerfile.development ├── FTGOGO.postman_collection.json ├── Makefile ├── README.md ├── accounting ├── acctmod │ └── setup.go ├── cmd │ ├── cdc │ │ └── main.go │ └── service │ │ ├── .env │ │ └── main.go ├── feature_test.go ├── features │ ├── authorize_order.feature │ ├── create_account.feature │ ├── disable_account.feature │ ├── enable_account.feature │ ├── get_account.feature │ └── steps │ │ ├── authorize_order.go │ │ ├── create_account.go │ │ ├── disable_account.go │ │ ├── enable_account.go │ │ ├── feature_state.go │ │ ├── get_account.go │ │ ├── reverse_authorize_order.go │ │ └── revise_authorize_order.go ├── go.mod ├── go.sum └── internal │ ├── adapters │ └── account_aggregate_repository.go │ ├── application │ ├── commands │ │ ├── authorize_order.go │ │ ├── create_account.go │ │ ├── disable_account.go │ │ ├── enable_account.go │ │ ├── reverse_authorize_order.go │ │ └── revise_authorize_order.go │ ├── ports │ │ └── account_repository.go │ ├── queries │ │ └── get_account.go │ └── service.go │ ├── domain │ ├── account.go │ ├── account_commands.go │ ├── account_events.go │ ├── account_snapshots.go │ └── register_types.go │ └── handlers │ ├── command_handlers.go │ ├── consumer_event_handlers.go │ ├── openapi.yaml │ └── rpc_handlers.go ├── config └── postgresql │ └── init-postgres.sql ├── consumer ├── cmd │ ├── cdc │ │ └── main.go │ └── service │ │ ├── .env │ │ └── main.go ├── consmod │ └── setup.go ├── feature_test.go ├── features │ ├── add_address.feature │ ├── get_address.feature │ ├── get_consumer.feature │ ├── register_consumer.feature │ ├── remove_address.feature │ ├── steps │ │ ├── add_address.go │ │ ├── feature_state.go │ │ ├── get_address.go │ │ ├── get_consumer.go │ │ ├── register_consumer.go │ │ ├── remove_address.go │ │ ├── update_address.go │ │ ├── update_consumer.go │ │ └── validate_order_by_consumer.go │ ├── update_address.feature │ ├── update_consumer.feature │ └── validate_order_by_consumer.feature ├── go.mod ├── go.sum └── internal │ ├── adapters │ ├── consumer_aggregate_repository.go │ ├── consumer_entity_event_publisher.go │ └── consumer_repository_publisher_middleware.go │ ├── application │ ├── commands │ │ ├── add_address.go │ │ ├── register_consumer.go │ │ ├── remove_address.go │ │ ├── update_address.go │ │ ├── update_consumer.go │ │ └── validate_order_by_consumer.go │ ├── ports │ │ ├── consumer_publisher.go │ │ └── consumer_repository.go │ ├── queries │ │ ├── get_address.go │ │ └── get_consumer.go │ └── service.go │ ├── domain │ ├── consumer.go │ ├── consumer_commands.go │ ├── consumer_snapshots.go │ └── register_types.go │ └── handlers │ ├── command_handlers.go │ ├── openapi.yaml │ └── rpc_handlers.go ├── customer-web ├── cmd │ └── gateway │ │ └── main.go ├── cwebmod │ └── setup.go ├── go.mod ├── go.sum └── internal │ ├── adapters │ ├── consumer_grpc_repository.go │ ├── consumer_repository.go │ ├── order_grpc_repository.go │ ├── order_history_grpc_repository.go │ ├── order_history_repository.go │ └── order_repository.go │ ├── application │ ├── commands │ │ ├── add_consumer_address.go │ │ ├── cancel_order.go │ │ ├── create_order.go │ │ ├── register_consumer.go │ │ ├── remove_consumer_address.go │ │ ├── revise_order.go │ │ └── update_consumer_address.go │ ├── queries │ │ ├── get_consumer.go │ │ ├── get_consumer_address.go │ │ ├── get_order.go │ │ └── search_orders.go │ └── service.go │ ├── domain │ ├── consumer.go │ ├── order.go │ └── order_history.go │ └── handlers │ ├── oapi-codegen.cfg.yaml │ ├── openapi.yaml │ ├── web_handlers.go │ └── web_server_api.gen.go ├── delivery ├── cmd │ └── service │ │ ├── .env │ │ └── main.go ├── delvmod │ └── setup.go ├── feature_test.go ├── features │ ├── cancel_delivery.feature │ ├── create_delivery.feature │ ├── create_restaurant.feature │ ├── get_courier.feature │ ├── get_delivery.feature │ ├── schedule_delivery.feature │ ├── set_courier_availability.feature │ └── steps │ │ ├── cancel_delivery.go │ │ ├── create_delivery.go │ │ ├── create_restaurant.go │ │ ├── feature_state.go │ │ ├── get_courier.go │ │ ├── get_delivery.go │ │ ├── schedule_delivery.go │ │ └── set_courier_availability.go ├── go.mod ├── go.sum └── internal │ ├── adapters │ ├── courier_inmem_repository.go │ ├── courier_postgres_repository.go │ ├── delivery_inmem_repository.go │ ├── delivery_postgres_repository.go │ ├── restaurant_inmem_repository.go │ └── restaurant_postgres_repository.go │ ├── application │ ├── commands │ │ ├── cancel_delivery.go │ │ ├── create_delivery.go │ │ ├── create_restaurant.go │ │ ├── schedule_delivery.go │ │ └── set_courier_availability.go │ ├── ports │ │ ├── courier_repository.go │ │ ├── delivery_repository.go │ │ └── restaurant_repository.go │ ├── queries │ │ ├── get_courier.go │ │ └── get_delivery.go │ └── service.go │ ├── domain │ ├── courier.go │ ├── delivery.go │ └── restaurant.go │ └── handlers │ ├── openapi.yaml │ ├── order_event_handlers.go │ ├── restaurant_event_handlers.go │ ├── rpc_handlers.go │ └── ticket_event_handlers.go ├── deployment └── kubernetes │ ├── accounting-service.yaml │ ├── config-map.yaml │ ├── namespace.yaml │ ├── postgresql.yaml │ └── stan.yaml ├── docker-compose-monolith.yml ├── docker-compose-use-kafka.yml ├── docker-compose.development.yml ├── docker-compose.yml ├── docs ├── architecture.png ├── cancelOrderSaga.png ├── createOrderSaga.png ├── diagrams.plantuml ├── hexagonal_architecture_w.png ├── outbox_pattern_bg.png ├── reviseOrderSaga.png └── theme.plantuml ├── kitchen ├── cmd │ ├── cdc │ │ └── main.go │ └── service │ │ ├── .env │ │ └── main.go ├── feature_test.go ├── features │ ├── accept_ticket.feature │ ├── cancel_ticket.feature │ ├── create_ticket.feature │ ├── get_ticket.feature │ ├── revise_ticket.feature │ └── steps │ │ ├── accept_ticket.go │ │ ├── cancel_ticket.go │ │ ├── create_restaurant.go │ │ ├── create_ticket.go │ │ ├── feature_state.go │ │ ├── get_ticket.go │ │ └── revise_ticket.go ├── go.mod ├── go.sum ├── internal │ ├── adapters │ │ ├── restaurant_inmem_repository.go │ │ ├── restaurant_postgres_repository.go │ │ ├── ticket_aggregate_repository.go │ │ ├── ticket_entity_event_publisher.go │ │ └── ticket_repository_publisher_middleware.go │ ├── application │ │ ├── commands │ │ │ ├── accept_ticket.go │ │ │ ├── begin_cancel_ticket.go │ │ │ ├── begin_revise_ticket.go │ │ │ ├── cancel_create_ticket.go │ │ │ ├── confirm_cancel_ticket.go │ │ │ ├── confirm_create_ticket.go │ │ │ ├── confirm_revise_ticket.go │ │ │ ├── create_restaurant.go │ │ │ ├── create_ticket.go │ │ │ ├── revise_restaurant_menu.go │ │ │ ├── undo_cancel_ticket.go │ │ │ └── undo_revise_ticket.go │ │ ├── ports │ │ │ ├── restaurant_repository.go │ │ │ ├── ticket_publisher.go │ │ │ └── ticket_repository.go │ │ ├── queries │ │ │ ├── get_restaurant.go │ │ │ └── get_ticket.go │ │ └── service.go │ ├── domain │ │ ├── register_types.go │ │ ├── restaurant.go │ │ ├── ticket.go │ │ ├── ticket_commands.go │ │ ├── ticket_events.go │ │ └── ticket_snapshots.go │ └── handlers │ │ ├── command_handlers.go │ │ ├── openapi.yaml │ │ ├── restaurant_event_handlers.go │ │ └── rpc_handlers.go └── kitcmod │ └── setup.go ├── monolith ├── cmd │ └── service │ │ ├── .env │ │ └── main.go ├── go.mod └── go.sum ├── order-history ├── cmd │ └── service │ │ ├── .env │ │ └── main.go ├── go.mod ├── go.sum ├── internal │ ├── adapters │ │ ├── order_history_postgres_repository.go │ │ └── order_history_repository.go │ ├── application │ │ ├── commands │ │ │ ├── create_order_history.go │ │ │ └── update_order_status.go │ │ ├── queries │ │ │ ├── get_order_history.go │ │ │ ├── search_order_histories.go │ │ │ └── spec.yaml │ │ └── service.go │ ├── domain │ │ └── order_history.go │ └── handlers │ │ ├── openapi.yaml │ │ ├── order_event_handlers.go │ │ └── rpc_handlers.go └── ohismod │ └── setup.go ├── order ├── cmd │ ├── cdc │ │ └── main.go │ └── service │ │ ├── .env │ │ └── main.go ├── feature_test.go ├── features │ ├── approve_order.feature │ ├── cancel_order.feature │ ├── create_order.feature │ ├── reject_order.feature │ ├── revise_order.feature │ └── steps │ │ ├── cancel_order.go │ │ ├── create_order.go │ │ ├── create_restaurant.go │ │ ├── feature_data.go │ │ ├── feature_state.go │ │ ├── get_order.go │ │ └── revise_order.go ├── go.mod ├── go.sum ├── internal │ ├── adapters │ │ ├── cancel_order_orchestration_saga.go │ │ ├── create_order_orchestration_saga.go │ │ ├── inmem_counter.go │ │ ├── order_aggregate_respository.go │ │ ├── order_entity_event_publisher.go │ │ ├── order_repository_publisher_middleware.go │ │ ├── prometheus_counter.go │ │ ├── restaurant_inmem_repository.go │ │ ├── restaurant_postgres_repository.go │ │ └── revise_order_orchestration_saga.go │ ├── application │ │ ├── commands │ │ │ ├── approve_order.go │ │ │ ├── begin_cancel_order.go │ │ │ ├── begin_revise_order.go │ │ │ ├── confirm_cancel_order.go │ │ │ ├── confirm_revise_order.go │ │ │ ├── create_order.go │ │ │ ├── create_restaurant.go │ │ │ ├── reject_order.go │ │ │ ├── revise_restaurant_menu.go │ │ │ ├── start_cancel_order_saga.go │ │ │ ├── start_create_order_saga.go │ │ │ ├── start_revise_order_saga.go │ │ │ ├── undo_cancel_order.go │ │ │ └── undo_revise_order.go │ │ ├── ports │ │ │ ├── cancel_order_saga.go │ │ │ ├── counter.go │ │ │ ├── create_order_saga.go │ │ │ ├── order_publisher.go │ │ │ ├── order_repository.go │ │ │ ├── restaurant_repository.go │ │ │ └── revise_order_saga.go │ │ ├── queries │ │ │ ├── get_order.go │ │ │ └── get_restaurant.go │ │ └── service.go │ ├── domain │ │ ├── cancel_order_saga_data.go │ │ ├── create_order_saga_data.go │ │ ├── order.go │ │ ├── order_commands.go │ │ ├── order_events.go │ │ ├── order_snapshots.go │ │ ├── register_types.go │ │ ├── restaurant.go │ │ └── revise_order_saga_data.go │ └── handlers │ │ ├── command_handlers.go │ │ ├── openapi.yaml │ │ ├── order_event_handlers.go │ │ ├── restaurant_event_handlers.go │ │ └── rpc_handlers.go └── ordmod │ └── setup.go ├── restaurant ├── cmd │ ├── cdc │ │ └── main.go │ └── service │ │ ├── .env │ │ └── main.go ├── feature_test.go ├── features │ ├── create_restaurant.feature │ └── steps │ │ ├── create_restaurant.go │ │ ├── feature_data.go │ │ └── feature_state.go ├── go.mod ├── go.sum ├── internal │ ├── adapters │ │ ├── restaurant_entity_event_publisher.go │ │ ├── restaurant_inmem_repository.go │ │ ├── restaurant_postgres_publisher_middleware.go │ │ └── restaurant_postgres_repository.go │ ├── application │ │ ├── commands │ │ │ └── create_restaurant.go │ │ ├── ports │ │ │ ├── restaurant_publisher.go │ │ │ └── restaurant_repository.go │ │ ├── queries │ │ │ └── get_restaurant.go │ │ └── service.go │ ├── domain │ │ └── restaurant.go │ └── handlers │ │ ├── openapi.yaml │ │ └── rpc_handlers.go └── restmod │ └── setup.go ├── serviceapis ├── Makefile ├── accountingapi │ ├── api.go │ ├── commands.go │ ├── pb │ │ ├── service.pb.go │ │ ├── service.proto │ │ └── service_grpc.pb.go │ └── replies.go ├── commonapi │ ├── entities.go │ ├── pb │ │ ├── service.pb.go │ │ └── service.proto │ ├── spec.gen.go │ └── spec.yaml ├── consumerapi │ ├── api.go │ ├── commands.go │ ├── events.go │ ├── pb │ │ ├── service.pb.go │ │ ├── service.proto │ │ └── service_grpc.pb.go │ └── replies.go ├── deliveryapi │ └── pb │ │ ├── service.pb.go │ │ ├── service.proto │ │ └── service_grpc.pb.go ├── go.mod ├── go.sum ├── kitchenapi │ ├── api.go │ ├── commands.go │ ├── entities.go │ ├── events.go │ ├── pb │ │ ├── service.pb.go │ │ ├── service.proto │ │ └── service_grpc.pb.go │ └── replies.go ├── orderapi │ ├── api.go │ ├── commands.go │ ├── entities.go │ ├── events.go │ ├── pb │ │ ├── service.pb.go │ │ ├── service.proto │ │ └── service_grpc.pb.go │ └── replies.go ├── orderhistoryapi │ └── pb │ │ ├── service.pb.go │ │ ├── service.proto │ │ └── service_grpc.pb.go ├── register_types.go └── restaurantapi │ ├── api.go │ ├── commands.go │ ├── entities.go │ ├── events.go │ ├── pb │ ├── service.pb.go │ ├── service.proto │ └── service_grpc.pb.go │ ├── replies.go │ └── spec.yaml ├── shared-go ├── applications │ ├── cdc.go │ ├── gateway.go │ ├── monolith.go │ ├── service.go │ └── shared.go ├── egress │ ├── options.go │ └── waiter.go ├── go.mod ├── go.sum ├── instrumentation │ ├── message.go │ └── web.go ├── logging │ ├── logger.go │ ├── zapto │ │ └── logger.go │ └── zerologto │ │ └── logger.go ├── rpc │ ├── client.go │ ├── client_options.go │ ├── logging.go │ ├── server.go │ └── server_options.go └── web │ ├── context.go │ ├── error_response.go │ ├── logging.go │ ├── server.go │ ├── server_options.go │ └── spec.yaml └── store-web ├── cmd └── gateway │ └── main.go ├── go.mod ├── go.sum └── internal ├── adapters ├── accounting_grpc_repository.go ├── accounting_repository.go ├── consumer_grpc_repository.go ├── consumer_repository.go ├── delivery_grpc_repository.go ├── delivery_repository.go ├── order_grpc_repository.go ├── order_repository.go ├── restaurant_grpc_repository.go └── restaurant_repository.go ├── application ├── commands │ ├── cancel_order.go │ ├── create_restaurant.go │ ├── disable_account.go │ ├── enable_account.go │ └── set_courier_availability.go ├── queries │ ├── get_account.go │ ├── get_consumer.go │ ├── get_delivery_history.go │ ├── get_order.go │ └── get_restaurant.go └── service.go ├── domain ├── account.go ├── consumer.go ├── courier.go ├── delivery.go ├── delivery_history.go ├── order.go └── restaurant.go └── handlers ├── oapi-codegen.cfg.yaml ├── openapi.yaml ├── web_handlers.go └── web_server_api.gen.go /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .git 4 | .idea 5 | local-data/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | tab_width = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | max_line_length = 120 12 | 13 | [*.md] 14 | indent_size = 4 15 | trim_trailing_whitespace = false 16 | 17 | [{*.go, *.go2}] 18 | indent_style = tab 19 | indent_size = 4 20 | tab_width = 4 21 | 22 | [*.{yml, yaml}] 23 | indent_style = tab 24 | 25 | [Makefile] 26 | indent_style = tab 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG service 2 | ARG cmd=service 3 | FROM golang:1.16-alpine AS builder 4 | ARG service 5 | ARG cmd 6 | 7 | WORKDIR $GOPATH/src/${service}/cmd/${cmd} 8 | COPY . $GOPATH/src 9 | RUN go install . 10 | 11 | FROM alpine:latest AS runtime 12 | ARG cmd 13 | ENV cmd=$cmd 14 | COPY --from=builder /go/bin/${cmd} /usr/local/bin/${cmd} 15 | 16 | ENTRYPOINT /usr/local/bin/${cmd} 17 | -------------------------------------------------------------------------------- /Dockerfile.development: -------------------------------------------------------------------------------- 1 | ARG service 2 | ARG cmd=service 3 | FROM golang:1.16-alpine AS builder 4 | ARG service 5 | ARG cmd 6 | 7 | COPY . $GOPATH/src 8 | WORKDIR $GOPATH/src/ftgogo/${service}/cmd/${cmd} 9 | RUN go install . 10 | 11 | FROM alpine:latest AS runtime 12 | ARG cmd 13 | ENV cmd=$cmd 14 | COPY --from=builder /go/bin/${cmd} /usr/local/bin/${cmd} 15 | 16 | ENTRYPOINT /usr/local/bin/${cmd} 17 | -------------------------------------------------------------------------------- /accounting/cmd/cdc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "shared-go/applications" 5 | ) 6 | 7 | func main() { 8 | cdc := applications.NewCDC(func(*applications.CDC) error { return nil }) 9 | if err := cdc.Execute(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /accounting/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=accounting-service -------------------------------------------------------------------------------- /accounting/cmd/service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/accounting/internal/adapters" 5 | "github.com/stackus/ftgogo/accounting/internal/application" 6 | "github.com/stackus/ftgogo/accounting/internal/domain" 7 | "github.com/stackus/ftgogo/accounting/internal/handlers" 8 | "github.com/stackus/ftgogo/serviceapis" 9 | "shared-go/applications" 10 | ) 11 | 12 | func main() { 13 | svc := applications.NewService(initService) 14 | if err := svc.Execute(); err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | func initService(svc *applications.Service) error { 20 | serviceapis.RegisterTypes() 21 | domain.RegisterTypes() 22 | 23 | // Driven 24 | accountRepo := adapters.NewAccountAggregateRepository(svc.AggregateStore) 25 | 26 | app := application.NewServiceApplication(accountRepo) 27 | 28 | // Drivers 29 | handlers.NewCommandHandlers(app).Mount(svc.Subscriber, svc.Publisher) 30 | handlers.NewConsumerEventHandlers(app).Mount(svc.Subscriber) 31 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /accounting/feature_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/cucumber/godog" 8 | flag "github.com/spf13/pflag" 9 | 10 | "github.com/stackus/ftgogo/accounting/features/steps" 11 | ) 12 | 13 | var opts = godog.Options{ 14 | Format: "progress", 15 | NoColors: true, 16 | } 17 | 18 | func init() { 19 | godog.BindCommandLineFlags("godog.", &opts) 20 | } 21 | 22 | func InitializeScenario(ctx *godog.ScenarioContext) { 23 | state := steps.NewFeatureState() 24 | 25 | ctx.BeforeScenario(func(*godog.Scenario) { 26 | state.Reset() 27 | }) 28 | 29 | state.RegisterCommonSteps(ctx) 30 | state.RegisterCreateAccountSteps(ctx) 31 | state.RegisterDisableAccountSteps(ctx) 32 | state.RegisterEnableAccountSteps(ctx) 33 | state.RegisterGetAccountSteps(ctx) 34 | state.RegisterAuthorizeOrderSteps(ctx) 35 | } 36 | 37 | func TestMain(m *testing.M) { 38 | flag.Parse() 39 | for _, arg := range os.Args[1:] { 40 | if arg == "-test.v=true" { 41 | opts.Format = "pretty" 42 | break 43 | } 44 | } 45 | 46 | status := godog.TestSuite{ 47 | Name: "accounting features", 48 | ScenarioInitializer: InitializeScenario, 49 | Options: &opts, 50 | }.Run() 51 | 52 | if st := m.Run(); st != 0 { 53 | os.Exit(st) 54 | } 55 | 56 | if status != 0 { 57 | os.Exit(status) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /accounting/features/authorize_order.feature: -------------------------------------------------------------------------------- 1 | @command 2 | Feature: Authorize Order 3 | 4 | Scenario: Orders are authorized on active accounts 5 | Given I create an account for the consumer "Able Anders" 6 | When I authorize an order totaling $9.99 for "Able Anders" 7 | Then I expect it be authorized 8 | 9 | Scenario: Orders are not authorized on inactive accounts 10 | Given I create an account for the consumer "Able Anders" 11 | And I disable the account for "Able Anders" 12 | When I authorize an order totaling $9.99 for "Able Anders" 13 | Then I don't expect it to be authorized 14 | And the returned error message is "account is disabled" 15 | 16 | Scenario: Orders are not authorized on unregistered accounts 17 | When I authorize an order totaling $9.99 for "Able Anders" 18 | Then I don't expect it to be authorized 19 | And the returned error message is "account not found" 20 | -------------------------------------------------------------------------------- /accounting/features/create_account.feature: -------------------------------------------------------------------------------- 1 | @command 2 | Feature: Create Account 3 | 4 | Scenario: Create a new account 5 | When I create an account for the consumer "Able Anders" 6 | Then I expect the command to succeed 7 | 8 | Scenario: Creating a duplicate account returns an error 9 | Given I create an account for the consumer "Able Anders" 10 | When I create an account for the consumer "Able Anders" 11 | Then I expect the command to fail 12 | And the returned error message is "account already exists" 13 | -------------------------------------------------------------------------------- /accounting/features/disable_account.feature: -------------------------------------------------------------------------------- 1 | @command 2 | Feature: Disable Accounts 3 | 4 | Scenario: Enabled accounts can be disabled 5 | Given I create an account for the consumer "Able Anders" 6 | When I disable the account for "Able Anders" 7 | Then I expect the command to succeed 8 | 9 | Scenario: Disabling already disabled accounts return an error 10 | Given I create an account for the consumer "Able Anders" 11 | And I disable the account for "Able Anders" 12 | When I disable the account for "Able Anders" 13 | Then I expect the command to fail 14 | And the returned error message is "account is disabled" 15 | 16 | Scenario: Disabling accounts that do not exist returns an error 17 | Given I create an account for the consumer "Able Anders" 18 | When I disable the account for "Betty Burns" 19 | Then I expect the command to fail 20 | And the returned error message is "account not found" 21 | -------------------------------------------------------------------------------- /accounting/features/enable_account.feature: -------------------------------------------------------------------------------- 1 | @command 2 | Feature: Enable Accounts 3 | 4 | Scenario: Disabled accounts can be re-enabled 5 | Given I create an account for the consumer "Able Anders" 6 | And I disable the account for "Able Anders" 7 | When I enable the account for "Able Anders" 8 | Then I expect the command to succeed 9 | 10 | Scenario: Enabling already enabled accounts return an error 11 | Given I create an account for the consumer "Able Anders" 12 | When I enable the account for "Able Anders" 13 | Then I expect the command to fail 14 | And the returned error message is "account is enabled" 15 | 16 | Scenario: Enabling accounts that do not exist returns an error 17 | Given I create an account for the consumer "Able Anders" 18 | And I disable the account for "Able Anders" 19 | When I enable the account for "Betty Burns" 20 | Then I expect the command to fail 21 | And the returned error message is "account not found" 22 | -------------------------------------------------------------------------------- /accounting/features/get_account.feature: -------------------------------------------------------------------------------- 1 | @query 2 | Feature: Get Accounts 3 | 4 | Scenario: Get an account by ID 5 | Given I create an account for the consumer "Able Anders" 6 | When I request the account for "Able Anders" 7 | Then I expect the request to succeed 8 | And the returned account is enabled 9 | 10 | Scenario: Get a disabled account by ID 11 | Given I create an account for the consumer "Able Anders" 12 | And I disable the account for "Able Anders" 13 | When I request the account for "Able Anders" 14 | Then I expect the request to succeed 15 | And the returned account is disabled 16 | 17 | Scenario: Getting an account that does not exist returns an error 18 | Given I create an account for the consumer "Able Anders" 19 | When I request the account for "Betty Burns" 20 | Then I expect the request to fail 21 | And the returned error message is "account not found" 22 | -------------------------------------------------------------------------------- /accounting/features/steps/authorize_order.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | "github.com/google/uuid" 8 | 9 | "github.com/stackus/ftgogo/accounting/internal/application/commands" 10 | ) 11 | 12 | func (f *FeatureState) RegisterAuthorizeOrderSteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^I authorize an order totaling \$(\d+)\.(\d+) for "([^"]*)"$`, f.iAuthorizeAnOrderTotalingFor) 14 | ctx.Step(`^I expect it be authorized$`, f.iExpectTheCommandToSucceed) 15 | ctx.Step(`^I don\'t expect it to be authorized$`, f.iExpectTheCommandToFail) 16 | } 17 | 18 | func (f *FeatureState) iAuthorizeAnOrderTotalingFor(dollars, cents int, consumerName string) error { 19 | orderID := uuid.New().String() 20 | consumerID := f.accountNames[consumerName] 21 | 22 | cmd := commands.AuthorizeOrder{ 23 | ConsumerID: consumerID, 24 | OrderID: orderID, 25 | OrderTotal: dollars*10 + cents, 26 | } 27 | 28 | f.err = f.app.AuthorizeOrder(context.Background(), cmd) 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /accounting/features/steps/create_account.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | "github.com/google/uuid" 8 | 9 | "github.com/stackus/ftgogo/accounting/internal/application/commands" 10 | ) 11 | 12 | func (f *FeatureState) RegisterCreateAccountSteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^I create an account for the consumer "([^"]*)"$`, f.iCreateAnAccountForTheConsumer) 14 | } 15 | 16 | func (f *FeatureState) iCreateAnAccountForTheConsumer(consumerName string) error { 17 | var consumerID string 18 | 19 | consumerID = f.accountNames[consumerName] 20 | if consumerID == "" { 21 | consumerID = uuid.New().String() 22 | f.accountNames[consumerName] = consumerID 23 | } 24 | 25 | cmd := commands.CreateAccount{ 26 | ConsumerID: consumerID, 27 | Name: consumerName, 28 | } 29 | 30 | f.err = f.app.CreateAccount(context.Background(), cmd) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /accounting/features/steps/disable_account.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/accounting/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterDisableAccountSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I disable the account for "([^"]*)"$`, f.iDisableTheAccountFor) 13 | } 14 | 15 | func (f *FeatureState) iDisableTheAccountFor(consumerName string) error { 16 | accountID := f.accountNames[consumerName] 17 | 18 | cmd := commands.DisableAccount{AccountID: accountID} 19 | 20 | f.err = f.app.DisableAccount(context.Background(), cmd) 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /accounting/features/steps/enable_account.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/accounting/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterEnableAccountSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I enable the account for "([^"]*)"$`, f.iEnableTheAccountFor) 13 | } 14 | 15 | func (f *FeatureState) iEnableTheAccountFor(consumerName string) error { 16 | accountID := f.accountNames[consumerName] 17 | 18 | cmd := commands.EnableAccount{AccountID: accountID} 19 | 20 | f.err = f.app.EnableAccount(context.Background(), cmd) 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /accounting/features/steps/reverse_authorize_order.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | // TODO test a noop command? 4 | -------------------------------------------------------------------------------- /accounting/features/steps/revise_authorize_order.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | // TODO test a noop command? 4 | -------------------------------------------------------------------------------- /accounting/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/accounting 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/cucumber/godog v0.11.0 16 | github.com/google/uuid v1.3.0 17 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 18 | github.com/hashicorp/go-memdb v1.3.2 // indirect 19 | github.com/mattn/go-colorable v0.1.8 // indirect 20 | github.com/spf13/pflag v1.0.5 21 | github.com/stackus/edat v0.0.6 22 | github.com/stackus/edat-msgpack v0.0.2 23 | github.com/stackus/edat-pgx v0.0.2 24 | github.com/stackus/errors v0.0.3 25 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 26 | google.golang.org/grpc v1.39.0 27 | google.golang.org/protobuf v1.27.1 28 | shared-go v0.0.0-00010101000000-000000000000 29 | ) 30 | -------------------------------------------------------------------------------- /accounting/internal/application/commands/authorize_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 7 | "github.com/stackus/ftgogo/accounting/internal/domain" 8 | ) 9 | 10 | type AuthorizeOrder struct { 11 | ConsumerID string 12 | OrderID string 13 | OrderTotal int 14 | } 15 | 16 | type AuthorizeOrderHandler struct { 17 | repo ports.AccountRepository 18 | } 19 | 20 | func NewAuthorizeOrderHandler(accountRepo ports.AccountRepository) AuthorizeOrderHandler { 21 | return AuthorizeOrderHandler{repo: accountRepo} 22 | } 23 | 24 | func (h AuthorizeOrderHandler) Handle(ctx context.Context, cmd AuthorizeOrder) error { 25 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.AuthorizeOrder{ 26 | OrderID: cmd.OrderID, 27 | OrderTotal: cmd.OrderTotal, 28 | }) 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /accounting/internal/application/commands/create_account.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/es" 7 | "github.com/stackus/errors" 8 | 9 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 10 | "github.com/stackus/ftgogo/accounting/internal/domain" 11 | ) 12 | 13 | type CreateAccount struct { 14 | ConsumerID string 15 | Name string 16 | } 17 | 18 | type CreateAccountHandler struct { 19 | repo ports.AccountRepository 20 | } 21 | 22 | func NewCreateAccountHandler(accountRepo ports.AccountRepository) CreateAccountHandler { 23 | return CreateAccountHandler{repo: accountRepo} 24 | } 25 | 26 | func (h CreateAccountHandler) Handle(ctx context.Context, cmd CreateAccount) error { 27 | _, err := h.repo.Save(ctx, &domain.CreateAccount{ 28 | Name: cmd.Name, 29 | }, es.WithAggregateRootID(cmd.ConsumerID)) 30 | 31 | // TODO update edat-pgx to return an es.ErrVersionConflict when versions do not align 32 | // for now assume all errors are duplicate account errors 33 | if err != nil { 34 | return errors.Wrap(errors.ErrConflict, "account already exists") 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /accounting/internal/application/commands/disable_account.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 7 | "github.com/stackus/ftgogo/accounting/internal/domain" 8 | ) 9 | 10 | type DisableAccount struct { 11 | AccountID string 12 | } 13 | 14 | type DisableAccountHandler struct { 15 | repo ports.AccountRepository 16 | } 17 | 18 | func NewDisableAccountHandler(accountRepo ports.AccountRepository) DisableAccountHandler { 19 | return DisableAccountHandler{repo: accountRepo} 20 | } 21 | 22 | func (h DisableAccountHandler) Handle(ctx context.Context, cmd DisableAccount) error { 23 | _, err := h.repo.Update(ctx, cmd.AccountID, &domain.DisableAccount{}) 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /accounting/internal/application/commands/enable_account.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 7 | "github.com/stackus/ftgogo/accounting/internal/domain" 8 | ) 9 | 10 | type EnableAccount struct { 11 | AccountID string 12 | } 13 | 14 | type EnableAccountHandler struct { 15 | repo ports.AccountRepository 16 | } 17 | 18 | func NewEnableAccountHandler(accountRepo ports.AccountRepository) EnableAccountHandler { 19 | return EnableAccountHandler{repo: accountRepo} 20 | } 21 | 22 | func (h EnableAccountHandler) Handle(ctx context.Context, cmd EnableAccount) error { 23 | _, err := h.repo.Update(ctx, cmd.AccountID, &domain.EnableAccount{}) 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /accounting/internal/application/commands/reverse_authorize_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 7 | "github.com/stackus/ftgogo/accounting/internal/domain" 8 | ) 9 | 10 | type ReverseAuthorizeOrder struct { 11 | ConsumerID string 12 | OrderID string 13 | OrderTotal int 14 | } 15 | 16 | type ReverseAuthorizeOrderHandler struct { 17 | repo ports.AccountRepository 18 | } 19 | 20 | func NewReverseAuthorizeOrderHandler(accountRepo ports.AccountRepository) ReverseAuthorizeOrderHandler { 21 | return ReverseAuthorizeOrderHandler{repo: accountRepo} 22 | } 23 | 24 | func (h ReverseAuthorizeOrderHandler) Handle(ctx context.Context, cmd ReverseAuthorizeOrder) error { 25 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.ReverseAuthorizeOrder{ 26 | OrderID: cmd.OrderID, 27 | OrderTotal: cmd.OrderTotal, 28 | }) 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /accounting/internal/application/commands/revise_authorize_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 7 | "github.com/stackus/ftgogo/accounting/internal/domain" 8 | ) 9 | 10 | type ReviseAuthorizeOrder struct { 11 | ConsumerID string 12 | OrderID string 13 | OrderTotal int 14 | } 15 | 16 | type ReviseAuthorizeOrderHandler struct { 17 | repo ports.AccountRepository 18 | } 19 | 20 | func NewReviseAuthorizeOrderHandler(accountRepo ports.AccountRepository) ReviseAuthorizeOrderHandler { 21 | return ReviseAuthorizeOrderHandler{repo: accountRepo} 22 | } 23 | 24 | func (h ReviseAuthorizeOrderHandler) Handle(ctx context.Context, cmd ReviseAuthorizeOrder) error { 25 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.ReviseAuthorizeOrder{ 26 | OrderID: cmd.OrderID, 27 | OrderTotal: cmd.OrderTotal, 28 | }) 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /accounting/internal/application/ports/account_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/es" 8 | 9 | "github.com/stackus/ftgogo/accounting/internal/domain" 10 | ) 11 | 12 | type AccountRepository interface { 13 | Load(ctx context.Context, aggregateID string) (*domain.Account, error) 14 | Save(ctx context.Context, command core.Command, options ...es.AggregateRootOption) (*domain.Account, error) 15 | Update(ctx context.Context, aggregateID string, command core.Command, options ...es.AggregateRootOption) (*domain.Account, error) 16 | } 17 | -------------------------------------------------------------------------------- /accounting/internal/application/queries/get_account.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/accounting/internal/application/ports" 7 | "github.com/stackus/ftgogo/accounting/internal/domain" 8 | ) 9 | 10 | type GetAccount struct { 11 | AccountID string 12 | } 13 | 14 | type GetAccountHandler struct { 15 | repo ports.AccountRepository 16 | } 17 | 18 | func NewGetAccountHandler(accountRepo ports.AccountRepository) GetAccountHandler { 19 | return GetAccountHandler{repo: accountRepo} 20 | } 21 | 22 | func (h GetAccountHandler) Handle(ctx context.Context, query GetAccount) (*domain.Account, error) { 23 | return h.repo.Load(ctx, query.AccountID) 24 | } 25 | -------------------------------------------------------------------------------- /accounting/internal/domain/account_commands.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerAccountCommands() { 8 | core.RegisterCommands( 9 | CreateAccount{}, 10 | AuthorizeOrder{}, ReverseAuthorizeOrder{}, ReviseAuthorizeOrder{}, 11 | DisableAccount{}, EnableAccount{}, 12 | ) 13 | } 14 | 15 | type CreateAccount struct { 16 | Name string 17 | } 18 | 19 | func (CreateAccount) CommandName() string { return "accountingservice.CreateAccount" } 20 | 21 | type AuthorizeOrder struct { 22 | OrderID string 23 | OrderTotal int 24 | } 25 | 26 | func (AuthorizeOrder) CommandName() string { return "accountingservice.AuthorizeOrder" } 27 | 28 | type ReverseAuthorizeOrder struct { 29 | OrderID string 30 | OrderTotal int 31 | } 32 | 33 | func (ReverseAuthorizeOrder) CommandName() string { return "accountingservice.ReverseAuthorizeOrder" } 34 | 35 | type ReviseAuthorizeOrder struct { 36 | OrderID string 37 | OrderTotal int 38 | } 39 | 40 | func (ReviseAuthorizeOrder) CommandName() string { return "accountingservice.ReviseAuthorizeOrder" } 41 | 42 | type DisableAccount struct{} 43 | 44 | func (DisableAccount) CommandName() string { return "accountingservice.DisableAccount" } 45 | 46 | type EnableAccount struct{} 47 | 48 | func (EnableAccount) CommandName() string { return "accountingservice.EnableAccount" } 49 | -------------------------------------------------------------------------------- /accounting/internal/domain/account_events.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerAccountEvents() { 8 | core.RegisterEvents( 9 | AccountCreated{}, OrderAuthorized{}, 10 | AccountDisabled{}, AccountEnabled{}, 11 | ) 12 | } 13 | 14 | type AccountCreated struct { 15 | Name string 16 | } 17 | 18 | func (AccountCreated) EventName() string { return "accountingservice.AccountCreated" } 19 | 20 | type OrderAuthorized struct { 21 | OrderID string 22 | OrderTotal int 23 | } 24 | 25 | func (OrderAuthorized) EventName() string { return "accountingservice.OrderAuthorized" } 26 | 27 | type AccountDisabled struct{} 28 | 29 | func (AccountDisabled) EventName() string { return "accountingservice.AccountDisabled" } 30 | 31 | type AccountEnabled struct{} 32 | 33 | func (AccountEnabled) EventName() string { return "accountingservice.AccountEnabled" } 34 | -------------------------------------------------------------------------------- /accounting/internal/domain/account_snapshots.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerAccountSnapshots() { 8 | core.RegisterSnapshots(AccountSnapshot{}) 9 | } 10 | 11 | type AccountSnapshot struct { 12 | Name string 13 | Enabled bool 14 | } 15 | 16 | func (AccountSnapshot) SnapshotName() string { return "accountingservice.AccountSnapshot" } 17 | -------------------------------------------------------------------------------- /accounting/internal/domain/register_types.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | func RegisterTypes() { 4 | registerAccountCommands() 5 | registerAccountEvents() 6 | registerAccountSnapshots() 7 | } 8 | -------------------------------------------------------------------------------- /accounting/internal/handlers/consumer_event_handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/msg" 7 | 8 | "github.com/stackus/ftgogo/accounting/internal/application" 9 | "github.com/stackus/ftgogo/accounting/internal/application/commands" 10 | "github.com/stackus/ftgogo/serviceapis/consumerapi" 11 | ) 12 | 13 | type ConsumerEventHandlers struct { 14 | app application.ServiceApplication 15 | } 16 | 17 | func NewConsumerEventHandlers(app application.ServiceApplication) ConsumerEventHandlers { 18 | return ConsumerEventHandlers{app: app} 19 | } 20 | 21 | func (h ConsumerEventHandlers) Mount(subscriber *msg.Subscriber) { 22 | subscriber.Subscribe(consumerapi.ConsumerAggregateChannel, msg.NewEntityEventDispatcher(). 23 | Handle(consumerapi.ConsumerRegistered{}, h.ConsumerRegistered)) 24 | } 25 | 26 | func (h ConsumerEventHandlers) ConsumerRegistered(ctx context.Context, evtMsg msg.EntityEvent) error { 27 | evt := evtMsg.Event().(*consumerapi.ConsumerRegistered) 28 | 29 | return h.app.CreateAccount(ctx, commands.CreateAccount{ 30 | ConsumerID: evtMsg.EntityID(), 31 | Name: evt.Name, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /consumer/cmd/cdc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "shared-go/applications" 5 | ) 6 | 7 | func main() { 8 | cdc := applications.NewCDC(func(*applications.CDC) error { return nil }) 9 | if err := cdc.Execute(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /consumer/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=consumer-service -------------------------------------------------------------------------------- /consumer/cmd/service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/consumer/internal/adapters" 5 | "github.com/stackus/ftgogo/consumer/internal/application" 6 | "github.com/stackus/ftgogo/consumer/internal/domain" 7 | "github.com/stackus/ftgogo/consumer/internal/handlers" 8 | "github.com/stackus/ftgogo/serviceapis" 9 | "shared-go/applications" 10 | ) 11 | 12 | func main() { 13 | svc := applications.NewService(initService) 14 | if err := svc.Execute(); err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | func initService(svc *applications.Service) error { 20 | serviceapis.RegisterTypes() 21 | domain.RegisterTypes() 22 | 23 | // Driven 24 | consumerRepo := adapters.NewConsumerRepositoryPublisherMiddleware( 25 | adapters.NewConsumerAggregateRepository(svc.AggregateStore), 26 | adapters.NewConsumerEntityEventPublisher(svc.Publisher), 27 | ) 28 | 29 | app := application.NewServiceApplication(consumerRepo) 30 | 31 | // Drivers 32 | handlers.NewCommandHandlers(app).Mount(svc.Subscriber, svc.Publisher) 33 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /consumer/features/register_consumer.feature: -------------------------------------------------------------------------------- 1 | @command @consumer 2 | Feature: Register Consumer 3 | 4 | Scenario: Consumers can be registered 5 | When I register a consumer named "Able Anders" 6 | Then I expect the command to succeed 7 | 8 | Scenario: Consumers must be registered with a name 9 | When I register a consumer named "" 10 | Then I expect the command to fail 11 | And the returned error message is "cannot register a consumer without a name" 12 | 13 | Scenario: Duplicate consumer names do not cause conflicts 14 | Given I register a consumer named "Able Anders" 15 | When I register another consumer named "Able Anders" 16 | Then I expect the command to succeed 17 | -------------------------------------------------------------------------------- /consumer/features/steps/add_address.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | "github.com/stackus/errors" 8 | 9 | "github.com/stackus/ftgogo/consumer/internal/application/commands" 10 | "github.com/stackus/ftgogo/serviceapis/commonapi" 11 | ) 12 | 13 | func (f *FeatureState) RegisterAddAddressSteps(ctx *godog.ScenarioContext) { 14 | ctx.Step(`^I add (?:an|another) address for "([^"]*)" with label "([^"]*)"$`, f.iAddAnAddressForWithLabel) 15 | } 16 | 17 | func (f *FeatureState) iAddAnAddressForWithLabel(consumerName, addressLabel string, table *godog.Table) error { 18 | consumerID := f.registeredConsumers[consumerName] 19 | 20 | address, err := assist.CreateInstance(new(commonapi.Address), table) 21 | if err != nil { 22 | return errors.Wrapf(errors.ErrUnknown, "error parsing address table: %w", err) 23 | } 24 | 25 | f.err = f.app.AddAddress(context.Background(), commands.AddAddress{ 26 | ConsumerID: consumerID, 27 | AddressID: addressLabel, 28 | Address: address.(*commonapi.Address), 29 | }) 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /consumer/features/steps/register_consumer.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/consumer/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterRegisterConsumerSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I register (?:a|another) consumer named "([^"]*)"$`, f.iRegisterAConsumerNamed) 13 | } 14 | 15 | func (f *FeatureState) iRegisterAConsumerNamed(consumerName string) error { 16 | cmd := commands.RegisterConsumer{ 17 | Name: consumerName, 18 | } 19 | 20 | f.consumerID, f.err = f.app.RegisterConsumer(context.Background(), cmd) 21 | f.registeredConsumers[consumerName] = f.consumerID 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /consumer/features/steps/remove_address.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/consumer/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterRemoveAddressSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I remove an address for "([^"]*)" with label "([^"]*)"$`, f.iRemoveAnAddressForWithLabel) 13 | } 14 | 15 | func (f *FeatureState) iRemoveAnAddressForWithLabel(consumerName, addressLabel string) error { 16 | consumerID := f.registeredConsumers[consumerName] 17 | 18 | f.err = f.app.RemoveAddress(context.Background(), commands.RemoveAddress{ 19 | ConsumerID: consumerID, 20 | AddressID: addressLabel, 21 | }) 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /consumer/features/steps/update_address.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | "github.com/stackus/errors" 8 | 9 | "github.com/stackus/ftgogo/consumer/internal/application/commands" 10 | "github.com/stackus/ftgogo/serviceapis/commonapi" 11 | ) 12 | 13 | func (f *FeatureState) RegisterUpdateAddressSteps(ctx *godog.ScenarioContext) { 14 | ctx.Step(`^I update an address for "([^"]*)" with label "([^"]*)"$`, f.iUpdateAnAddressForWithLabel) 15 | } 16 | 17 | func (f *FeatureState) iUpdateAnAddressForWithLabel(consumerName, addressLabel string, table *godog.Table) error { 18 | consumerID := f.registeredConsumers[consumerName] 19 | 20 | address, err := assist.CreateInstance(new(commonapi.Address), table) 21 | if err != nil { 22 | return errors.Wrapf(errors.ErrUnknown, "error parsing address table: %w", err) 23 | } 24 | 25 | f.err = f.app.UpdateAddress(context.Background(), commands.UpdateAddress{ 26 | ConsumerID: consumerID, 27 | AddressID: addressLabel, 28 | Address: address.(*commonapi.Address), 29 | }) 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /consumer/features/steps/update_consumer.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/consumer/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterUpdateConsumerSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I change "([^"]*)" name to "([^"]*)"$`, f.iChangeNameTo) 13 | } 14 | 15 | func (f *FeatureState) iChangeNameTo(currentName, newName string) error { 16 | consumerID := f.registeredConsumers[currentName] 17 | 18 | f.err = f.app.UpdateConsumer(context.Background(), commands.UpdateConsumer{ 19 | ConsumerID: consumerID, 20 | Name: newName, 21 | }) 22 | 23 | if f.err == nil { 24 | delete(f.registeredConsumers, currentName) 25 | f.registeredConsumers[newName] = consumerID 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /consumer/features/update_consumer.feature: -------------------------------------------------------------------------------- 1 | @command @consumer 2 | Feature: Update Consumers 3 | 4 | Background: Setup a consumer 5 | Given I register a consumer named "Able Anders" 6 | 7 | Scenario: Consumers can be updated 8 | When I change "Able Anders" name to "Anders Able" 9 | Then I expect the command to succeed 10 | 11 | Scenario: Consumer names are changed 12 | Given I change "Able Anders" name to "Anders Able" 13 | When I request the consumer named "Anders Able" 14 | Then I expect the request to succeed 15 | And the returned consumer has the name "Anders Able" 16 | 17 | Scenario: Updating consumers that do not exist returns an error 18 | When I change "Betty Burns" name to "Burns Betty" 19 | Then I expect the command to fail 20 | And the returned error message is "consumer not found" 21 | -------------------------------------------------------------------------------- /consumer/features/validate_order_by_consumer.feature: -------------------------------------------------------------------------------- 1 | @command @consumer @order 2 | Feature: Validate Orders By Consumer 3 | 4 | Background: Setup a consumer 5 | Given I register a consumer named "Able Anders" 6 | 7 | Scenario: Can validate orders for consumers 8 | When I validate an order for "Able Anders" 9 | | OrderID | A123 | 10 | | Total | $9.99 | 11 | Then I expect the command to succeed 12 | 13 | Scenario: Cannot validate orders for consumers that do not exist 14 | When I validate an order for "Betty Burns" 15 | | OrderID | A123 | 16 | | Total | $9.99 | 17 | Then I expect the command to fail 18 | And the returned error message is "consumer not found" 19 | -------------------------------------------------------------------------------- /consumer/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/consumer 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/cucumber/godog v0.11.0 16 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 17 | github.com/hashicorp/go-memdb v1.3.2 // indirect 18 | github.com/mattn/go-colorable v0.1.8 // indirect 19 | github.com/rdumont/assistdog v0.0.0-20201106100018-168b06230d14 20 | github.com/spf13/pflag v1.0.5 21 | github.com/stackus/edat v0.0.6 22 | github.com/stackus/edat-msgpack v0.0.2 23 | github.com/stackus/edat-pgx v0.0.2 24 | github.com/stackus/errors v0.0.3 25 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 26 | google.golang.org/grpc v1.39.0 27 | google.golang.org/protobuf v1.27.1 28 | shared-go v0.0.0-00010101000000-000000000000 29 | ) 30 | -------------------------------------------------------------------------------- /consumer/internal/adapters/consumer_entity_event_publisher.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | ) 8 | 9 | func NewConsumerEntityEventPublisher(publisher msg.EntityEventMessagePublisher) ports.ConsumerPublisher { 10 | return publisher 11 | } 12 | -------------------------------------------------------------------------------- /consumer/internal/application/commands/add_address.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | ) 10 | 11 | type AddAddress struct { 12 | ConsumerID string 13 | AddressID string 14 | Address *commonapi.Address 15 | } 16 | 17 | type AddAddressHandler struct { 18 | repo ports.ConsumerRepository 19 | } 20 | 21 | func NewAddAddressHandler(repo ports.ConsumerRepository) AddAddressHandler { 22 | return AddAddressHandler{repo: repo} 23 | } 24 | 25 | func (h AddAddressHandler) Handle(ctx context.Context, cmd AddAddress) error { 26 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.AddAddress{ 27 | AddressID: cmd.AddressID, 28 | Address: cmd.Address, 29 | }) 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /consumer/internal/application/commands/register_consumer.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | ) 9 | 10 | type RegisterConsumer struct { 11 | Name string 12 | } 13 | 14 | type RegisterConsumerHandler struct { 15 | repo ports.ConsumerRepository 16 | } 17 | 18 | func NewRegisterConsumerHandler(repo ports.ConsumerRepository) RegisterConsumerHandler { 19 | return RegisterConsumerHandler{ 20 | repo: repo, 21 | } 22 | } 23 | 24 | func (h RegisterConsumerHandler) Handle(ctx context.Context, cmd RegisterConsumer) (string, error) { 25 | consumer, err := h.repo.Save(ctx, &domain.RegisterConsumer{ 26 | Name: cmd.Name, 27 | }) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | return consumer.ID(), nil 33 | } 34 | -------------------------------------------------------------------------------- /consumer/internal/application/commands/remove_address.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | ) 9 | 10 | type RemoveAddress struct { 11 | ConsumerID string 12 | AddressID string 13 | } 14 | 15 | type RemoveAddressHandler struct { 16 | repo ports.ConsumerRepository 17 | } 18 | 19 | func NewRemoveAddressHandler(repo ports.ConsumerRepository) RemoveAddressHandler { 20 | return RemoveAddressHandler{repo: repo} 21 | } 22 | 23 | func (h RemoveAddressHandler) Handle(ctx context.Context, cmd RemoveAddress) error { 24 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.RemoveAddress{ 25 | AddressID: cmd.AddressID, 26 | }) 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /consumer/internal/application/commands/update_address.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | ) 10 | 11 | type UpdateAddress struct { 12 | ConsumerID string 13 | AddressID string 14 | Address *commonapi.Address 15 | } 16 | 17 | type UpdateAddressHandler struct { 18 | repo ports.ConsumerRepository 19 | } 20 | 21 | func NewUpdateAddressHandler(repo ports.ConsumerRepository) UpdateAddressHandler { 22 | return UpdateAddressHandler{repo: repo} 23 | } 24 | 25 | func (h UpdateAddressHandler) Handle(ctx context.Context, cmd UpdateAddress) error { 26 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.UpdateAddress{ 27 | AddressID: cmd.AddressID, 28 | Address: cmd.Address, 29 | }) 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /consumer/internal/application/commands/update_consumer.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | ) 9 | 10 | type UpdateConsumer struct { 11 | ConsumerID string 12 | Name string 13 | } 14 | 15 | type UpdateConsumerHandler struct { 16 | repo ports.ConsumerRepository 17 | } 18 | 19 | func NewUpdateConsumerHandler(repo ports.ConsumerRepository) UpdateConsumerHandler { 20 | return UpdateConsumerHandler{repo: repo} 21 | } 22 | 23 | func (h UpdateConsumerHandler) Handle(ctx context.Context, cmd UpdateConsumer) error { 24 | _, err := h.repo.Update(ctx, cmd.ConsumerID, &domain.UpdateConsumer{Name: cmd.Name}) 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /consumer/internal/application/commands/validate_order_by_consumer.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | ) 9 | 10 | type ValidateOrderByConsumer struct { 11 | ConsumerID string 12 | OrderID string 13 | OrderTotal int 14 | } 15 | 16 | type ValidateOrderByConsumerHandler struct { 17 | repo ports.ConsumerRepository 18 | } 19 | 20 | func NewValidateOrderByConsumerHandler(consumerRepo ports.ConsumerRepository) ValidateOrderByConsumerHandler { 21 | return ValidateOrderByConsumerHandler{repo: consumerRepo} 22 | } 23 | 24 | func (h ValidateOrderByConsumerHandler) Handle(ctx context.Context, cmd ValidateOrderByConsumer) error { 25 | consumer, err := h.repo.Load(ctx, cmd.ConsumerID) 26 | if err != nil { 27 | return domain.ErrConsumerNotFound 28 | } 29 | 30 | err = consumer.ValidateOrderByConsumer(cmd.OrderTotal) 31 | if err != nil { 32 | return domain.ErrOrderNotValidated 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /consumer/internal/application/ports/consumer_publisher.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | ) 6 | 7 | type ConsumerPublisher interface { 8 | msg.EntityEventMessagePublisher 9 | } 10 | -------------------------------------------------------------------------------- /consumer/internal/application/ports/consumer_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/es" 8 | 9 | "github.com/stackus/ftgogo/consumer/internal/domain" 10 | ) 11 | 12 | type ConsumerRepository interface { 13 | Load(ctx context.Context, aggregateID string) (*domain.Consumer, error) 14 | Save(ctx context.Context, command core.Command, options ...es.AggregateRootOption) (*domain.Consumer, error) 15 | Update(ctx context.Context, aggregateID string, command core.Command, options ...es.AggregateRootOption) (*domain.Consumer, error) 16 | } 17 | -------------------------------------------------------------------------------- /consumer/internal/application/queries/get_address.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/errors" 7 | 8 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 9 | "github.com/stackus/ftgogo/serviceapis/commonapi" 10 | ) 11 | 12 | type GetAddress struct { 13 | ConsumerID string 14 | AddressID string 15 | } 16 | 17 | type GetAddressHandler struct { 18 | repo ports.ConsumerRepository 19 | } 20 | 21 | func NewGetAddressHandler(repo ports.ConsumerRepository) GetAddressHandler { 22 | return GetAddressHandler{repo: repo} 23 | } 24 | 25 | func (h GetAddressHandler) Handle(ctx context.Context, query GetAddress) (*commonapi.Address, error) { 26 | consumer, err := h.repo.Load(ctx, query.ConsumerID) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | address := consumer.Address(query.AddressID) 32 | if address == nil { 33 | return nil, errors.Wrap(errors.ErrNotFound, "an address with that identifier does not exist") 34 | } 35 | 36 | return address, nil 37 | } 38 | -------------------------------------------------------------------------------- /consumer/internal/application/queries/get_consumer.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/consumer/internal/application/ports" 7 | "github.com/stackus/ftgogo/consumer/internal/domain" 8 | ) 9 | 10 | type GetConsumer struct { 11 | ConsumerID string 12 | } 13 | 14 | type GetConsumerHandler struct { 15 | repo ports.ConsumerRepository 16 | } 17 | 18 | func NewGetConsumerHandler(consumerRepo ports.ConsumerRepository) GetConsumerHandler { 19 | return GetConsumerHandler{repo: consumerRepo} 20 | } 21 | 22 | func (h GetConsumerHandler) Handle(ctx context.Context, query GetConsumer) (*domain.Consumer, error) { 23 | return h.repo.Load(ctx, query.ConsumerID) 24 | } 25 | -------------------------------------------------------------------------------- /consumer/internal/domain/consumer_commands.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | ) 8 | 9 | func registerConsumerCommands() { 10 | core.RegisterCommands( 11 | RegisterConsumer{}, UpdateConsumer{}, 12 | AddAddress{}, UpdateAddress{}, RemoveAddress{}, 13 | ) 14 | } 15 | 16 | type RegisterConsumer struct { 17 | Name string 18 | } 19 | 20 | func (RegisterConsumer) CommandName() string { return "consumerservice.RegisterConsumer" } 21 | 22 | type UpdateConsumer struct { 23 | Name string 24 | } 25 | 26 | func (UpdateConsumer) CommandName() string { return "consumerservice.UpdateConsumer" } 27 | 28 | type AddAddress struct { 29 | AddressID string 30 | Address *commonapi.Address 31 | } 32 | 33 | func (AddAddress) CommandName() string { return "consumerservice.AddAddress" } 34 | 35 | type UpdateAddress struct { 36 | AddressID string 37 | Address *commonapi.Address 38 | } 39 | 40 | func (UpdateAddress) CommandName() string { return "consumerservice.UpdateAddress" } 41 | 42 | type RemoveAddress struct { 43 | AddressID string 44 | } 45 | 46 | func (RemoveAddress) CommandName() string { return "consumerservice.RemoveAddress" } 47 | -------------------------------------------------------------------------------- /consumer/internal/domain/consumer_snapshots.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | ) 8 | 9 | func registerConsumerSnapshots() { 10 | core.RegisterSnapshots(ConsumerSnapshot{}) 11 | } 12 | 13 | type ConsumerSnapshot struct { 14 | Name string 15 | Addresses map[string]*commonapi.Address 16 | } 17 | 18 | func (ConsumerSnapshot) SnapshotName() string { 19 | return "consumerservice.ConsumerSnapshot" 20 | } 21 | -------------------------------------------------------------------------------- /consumer/internal/domain/register_types.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | func RegisterTypes() { 4 | registerConsumerCommands() 5 | registerConsumerSnapshots() 6 | } 7 | -------------------------------------------------------------------------------- /customer-web/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/customer-web 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/deepmap/oapi-codegen v1.8.2 16 | github.com/getkin/kin-openapi v0.68.0 17 | github.com/go-chi/chi/v5 v5.0.3 18 | github.com/go-chi/jwtauth/v5 v5.0.1 19 | github.com/go-chi/render v1.0.1 20 | github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect 21 | github.com/lestrrat-go/jwx v1.2.4 22 | github.com/stackus/errors v0.0.3 23 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 24 | google.golang.org/protobuf v1.27.1 25 | shared-go v0.0.0-00010101000000-000000000000 26 | ) 27 | -------------------------------------------------------------------------------- /customer-web/internal/adapters/consumer_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/domain" 7 | "github.com/stackus/ftgogo/serviceapis/commonapi" 8 | ) 9 | 10 | type ( 11 | RegisterConsumer struct { 12 | Name string 13 | } 14 | 15 | FindConsumer struct { 16 | ConsumerID string 17 | } 18 | 19 | UpdateConsumer struct { 20 | Consumer domain.Consumer 21 | } 22 | 23 | AddConsumerAddress struct { 24 | ConsumerID string 25 | AddressID string 26 | Address *commonapi.Address 27 | } 28 | 29 | FindConsumerAddress struct { 30 | ConsumerID string 31 | AddressID string 32 | } 33 | 34 | UpdateConsumerAddress AddConsumerAddress 35 | RemoveConsumerAddress FindConsumerAddress 36 | ) 37 | 38 | type ConsumerRepository interface { 39 | Register(ctx context.Context, registerConsumer RegisterConsumer) (string, error) 40 | Find(ctx context.Context, findConsumer FindConsumer) (*domain.Consumer, error) 41 | Update(ctx context.Context, updateConsumer UpdateConsumer) error 42 | AddAddress(ctx context.Context, addAddress AddConsumerAddress) error 43 | FindAddress(ctx context.Context, findAddress FindConsumerAddress) (*commonapi.Address, error) 44 | UpdateAddress(ctx context.Context, updateAddress UpdateConsumerAddress) error 45 | RemoveAddress(ctx context.Context, removeAddress RemoveConsumerAddress) error 46 | } 47 | -------------------------------------------------------------------------------- /customer-web/internal/adapters/order_history_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/domain" 7 | ) 8 | 9 | type ( 10 | SearchOrders struct { 11 | ConsumerID string 12 | Filters *domain.SearchOrdersFilters 13 | Next string 14 | Limit int 15 | } 16 | 17 | SearchOrdersResult struct { 18 | Orders []*domain.OrderHistory 19 | Next string 20 | } 21 | ) 22 | 23 | type OrderHistoryRepository interface { 24 | SearchOrders(ctx context.Context, search SearchOrders) (*SearchOrdersResult, error) 25 | } 26 | -------------------------------------------------------------------------------- /customer-web/internal/adapters/order_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stackus/ftgogo/customer-web/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | "github.com/stackus/ftgogo/serviceapis/orderapi" 10 | ) 11 | 12 | type ( 13 | CreateOrder struct { 14 | ConsumerID string 15 | RestaurantID string 16 | DeliverAt time.Time 17 | DeliverTo *commonapi.Address 18 | LineItems commonapi.MenuItemQuantities 19 | } 20 | 21 | FindOrder struct { 22 | OrderID string 23 | } 24 | 25 | CancelOrder FindOrder 26 | 27 | ReviseOrder struct { 28 | OrderID string 29 | RevisedQuantities commonapi.MenuItemQuantities 30 | } 31 | ) 32 | 33 | type OrderRepository interface { 34 | Create(ctx context.Context, createOrder CreateOrder) (string, error) 35 | Find(ctx context.Context, findOrder FindOrder) (*domain.Order, error) 36 | Revise(ctx context.Context, reviseOrder ReviseOrder) (orderapi.OrderState, error) 37 | Cancel(ctx context.Context, cancelOrder CancelOrder) (orderapi.OrderState, error) 38 | } 39 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/add_consumer_address.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | "github.com/stackus/ftgogo/serviceapis/commonapi" 8 | ) 9 | 10 | type AddConsumerAddress struct { 11 | ConsumerID string 12 | AddressID string 13 | Address *commonapi.Address 14 | } 15 | 16 | type AddConsumerAddressHandler struct { 17 | repo adapters.ConsumerRepository 18 | } 19 | 20 | func NewAddConsumerAddressHandler(repo adapters.ConsumerRepository) AddConsumerAddressHandler { 21 | return AddConsumerAddressHandler{repo: repo} 22 | } 23 | 24 | func (h AddConsumerAddressHandler) Handle(ctx context.Context, cmd AddConsumerAddress) error { 25 | return h.repo.AddAddress(ctx, adapters.AddConsumerAddress{ 26 | ConsumerID: cmd.ConsumerID, 27 | AddressID: cmd.AddressID, 28 | Address: cmd.Address, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/cancel_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/errors" 7 | 8 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 9 | "github.com/stackus/ftgogo/serviceapis/orderapi" 10 | ) 11 | 12 | type CancelOrder struct { 13 | ConsumerID string 14 | OrderID string 15 | } 16 | 17 | type CancelOrderHandler struct { 18 | repo adapters.OrderRepository 19 | } 20 | 21 | func NewCancelOrderHandler(repo adapters.OrderRepository) CancelOrderHandler { 22 | return CancelOrderHandler{repo: repo} 23 | } 24 | 25 | func (h CancelOrderHandler) Handle(ctx context.Context, cmd CancelOrder) (orderapi.OrderState, error) { 26 | order, err := h.repo.Find(ctx, adapters.FindOrder{ 27 | OrderID: cmd.OrderID, 28 | }) 29 | if err != nil { 30 | return orderapi.UnknownOrderState, err 31 | } 32 | 33 | if order.ConsumerID != cmd.ConsumerID { 34 | // being opaque intentionally; Could also send a permission denied error 35 | return orderapi.UnknownOrderState, errors.Wrap(errors.ErrNotFound, "order not found") 36 | } 37 | 38 | return h.repo.Cancel(ctx, adapters.CancelOrder{ 39 | OrderID: cmd.OrderID, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/create_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | ) 10 | 11 | type CreateOrder struct { 12 | ConsumerID string 13 | RestaurantID string 14 | AddressID string 15 | LineItems commonapi.MenuItemQuantities 16 | } 17 | 18 | type CreateOrderHandler struct { 19 | orderRepo adapters.OrderRepository 20 | consumerRepo adapters.ConsumerRepository 21 | } 22 | 23 | func NewCreateOrderHandler(orderRepo adapters.OrderRepository, consumerRepo adapters.ConsumerRepository) CreateOrderHandler { 24 | return CreateOrderHandler{ 25 | orderRepo: orderRepo, 26 | consumerRepo: consumerRepo, 27 | } 28 | } 29 | 30 | func (h CreateOrderHandler) Handle(ctx context.Context, cmd CreateOrder) (string, error) { 31 | address, err := h.consumerRepo.FindAddress(ctx, adapters.FindConsumerAddress{ 32 | ConsumerID: cmd.ConsumerID, 33 | AddressID: cmd.AddressID, 34 | }) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | return h.orderRepo.Create(ctx, adapters.CreateOrder{ 40 | ConsumerID: cmd.ConsumerID, 41 | RestaurantID: cmd.RestaurantID, 42 | DeliverAt: time.Now().Add(30 * time.Minute), 43 | DeliverTo: address, 44 | LineItems: cmd.LineItems, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/register_consumer.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | ) 8 | 9 | type RegisterConsumer struct { 10 | Name string 11 | } 12 | 13 | type RegisterConsumerHandler struct { 14 | repo adapters.ConsumerRepository 15 | } 16 | 17 | func NewRegisterConsumerHandler(repo adapters.ConsumerRepository) RegisterConsumerHandler { 18 | return RegisterConsumerHandler{repo: repo} 19 | } 20 | 21 | func (h RegisterConsumerHandler) Handle(ctx context.Context, cmd RegisterConsumer) (string, error) { 22 | consumerID, err := h.repo.Register(ctx, adapters.RegisterConsumer{ 23 | Name: cmd.Name, 24 | }) 25 | if err != nil { 26 | return "", err 27 | } 28 | 29 | return consumerID, nil 30 | } 31 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/remove_consumer_address.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | ) 8 | 9 | type RemoveConsumerAddress struct { 10 | ConsumerID string 11 | AddressID string 12 | } 13 | 14 | type RemoveConsumerAddressHandler struct { 15 | repo adapters.ConsumerRepository 16 | } 17 | 18 | func NewRemoveConsumerAddressHandler(repo adapters.ConsumerRepository) RemoveConsumerAddressHandler { 19 | return RemoveConsumerAddressHandler{repo: repo} 20 | } 21 | 22 | func (h RemoveConsumerAddressHandler) Handle(ctx context.Context, cmd RemoveConsumerAddress) error { 23 | return h.repo.RemoveAddress(ctx, adapters.RemoveConsumerAddress{ 24 | ConsumerID: cmd.ConsumerID, 25 | AddressID: cmd.AddressID, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/revise_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/errors" 7 | 8 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 9 | "github.com/stackus/ftgogo/serviceapis/commonapi" 10 | "github.com/stackus/ftgogo/serviceapis/orderapi" 11 | ) 12 | 13 | type ReviseOrder struct { 14 | ConsumerID string 15 | OrderID string 16 | RevisedQuantities commonapi.MenuItemQuantities 17 | } 18 | 19 | type ReviseOrderHandler struct { 20 | repo adapters.OrderRepository 21 | } 22 | 23 | func NewReviseOrderHandler(repo adapters.OrderRepository) ReviseOrderHandler { 24 | return ReviseOrderHandler{repo: repo} 25 | } 26 | 27 | func (h ReviseOrderHandler) Handle(ctx context.Context, cmd ReviseOrder) (orderapi.OrderState, error) { 28 | order, err := h.repo.Find(ctx, adapters.FindOrder{ 29 | OrderID: cmd.OrderID, 30 | }) 31 | if err != nil { 32 | return orderapi.UnknownOrderState, err 33 | } 34 | 35 | if order.ConsumerID != cmd.ConsumerID { 36 | // being opaque intentionally; Could also send a permission denied error 37 | return orderapi.UnknownOrderState, errors.Wrap(errors.ErrNotFound, "order not found") 38 | } 39 | 40 | return h.repo.Revise(ctx, adapters.ReviseOrder{ 41 | OrderID: cmd.OrderID, 42 | RevisedQuantities: cmd.RevisedQuantities, 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /customer-web/internal/application/commands/update_consumer_address.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | "github.com/stackus/ftgogo/serviceapis/commonapi" 8 | ) 9 | 10 | type UpdateConsumerAddress struct { 11 | ConsumerID string 12 | AddressID string 13 | Address *commonapi.Address 14 | } 15 | 16 | type UpdateConsumerAddressHandler struct { 17 | repo adapters.ConsumerRepository 18 | } 19 | 20 | func NewUpdateConsumerAddressHandler(repo adapters.ConsumerRepository) UpdateConsumerAddressHandler { 21 | return UpdateConsumerAddressHandler{repo: repo} 22 | } 23 | 24 | func (h UpdateConsumerAddressHandler) Handle(ctx context.Context, cmd UpdateConsumerAddress) error { 25 | return h.repo.UpdateAddress(ctx, adapters.UpdateConsumerAddress{ 26 | ConsumerID: cmd.ConsumerID, 27 | AddressID: cmd.AddressID, 28 | Address: cmd.Address, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /customer-web/internal/application/queries/get_consumer.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | "github.com/stackus/ftgogo/customer-web/internal/domain" 8 | ) 9 | 10 | type GetConsumer struct { 11 | ConsumerID string 12 | } 13 | 14 | type GetConsumerHandler struct { 15 | repo adapters.ConsumerRepository 16 | } 17 | 18 | func NewGetConsumerHandler(repo adapters.ConsumerRepository) GetConsumerHandler { 19 | return GetConsumerHandler{repo: repo} 20 | } 21 | 22 | func (h GetConsumerHandler) Handle(ctx context.Context, cmd GetConsumer) (*domain.Consumer, error) { 23 | return h.repo.Find(ctx, adapters.FindConsumer{ 24 | ConsumerID: cmd.ConsumerID, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /customer-web/internal/application/queries/get_consumer_address.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | "github.com/stackus/ftgogo/serviceapis/commonapi" 8 | ) 9 | 10 | type GetConsumerAddress struct { 11 | ConsumerID string 12 | AddressID string 13 | } 14 | 15 | type GetConsumerAddressHandler struct { 16 | repo adapters.ConsumerRepository 17 | } 18 | 19 | func NewGetConsumerAddressHandler(repo adapters.ConsumerRepository) GetConsumerAddressHandler { 20 | return GetConsumerAddressHandler{repo: repo} 21 | } 22 | 23 | func (h GetConsumerAddressHandler) Handle(ctx context.Context, cmd GetConsumerAddress) (*commonapi.Address, error) { 24 | return h.repo.FindAddress(ctx, adapters.FindConsumerAddress{ 25 | ConsumerID: cmd.ConsumerID, 26 | AddressID: cmd.AddressID, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /customer-web/internal/application/queries/get_order.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/errors" 7 | 8 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 9 | "github.com/stackus/ftgogo/customer-web/internal/domain" 10 | ) 11 | 12 | type GetOrder struct { 13 | OrderID string 14 | ConsumerID string 15 | } 16 | 17 | type GetOrderHandler struct { 18 | repo adapters.OrderRepository 19 | } 20 | 21 | func NewGetOrderHandler(repo adapters.OrderRepository) GetOrderHandler { 22 | return GetOrderHandler{repo: repo} 23 | } 24 | 25 | func (h GetOrderHandler) Handle(ctx context.Context, query GetOrder) (*domain.Order, error) { 26 | order, err := h.repo.Find(ctx, adapters.FindOrder{ 27 | OrderID: query.OrderID, 28 | }) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if order.ConsumerID != query.ConsumerID { 34 | // being opaque intentionally; Could also send a permission denied error 35 | return nil, errors.Wrap(errors.ErrNotFound, "order not found") 36 | } 37 | 38 | return order, nil 39 | } 40 | -------------------------------------------------------------------------------- /customer-web/internal/application/queries/search_orders.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/customer-web/internal/adapters" 7 | "github.com/stackus/ftgogo/customer-web/internal/domain" 8 | ) 9 | 10 | type SearchOrders struct { 11 | ConsumerID string 12 | Filters *domain.SearchOrdersFilters 13 | Next string 14 | Limit int 15 | } 16 | 17 | type SearchOrdersHandler struct { 18 | repo adapters.OrderHistoryRepository 19 | } 20 | 21 | func NewSearchOrdersHandler(repo adapters.OrderHistoryRepository) SearchOrdersHandler { 22 | return SearchOrdersHandler{repo: repo} 23 | } 24 | 25 | func (h SearchOrdersHandler) Handle(ctx context.Context, cmd SearchOrders) (*adapters.SearchOrdersResult, error) { 26 | return h.repo.SearchOrders(ctx, adapters.SearchOrders{ 27 | ConsumerID: cmd.ConsumerID, 28 | Filters: cmd.Filters, 29 | Next: cmd.Next, 30 | Limit: cmd.Limit, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /customer-web/internal/application/service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/customer-web/internal/application/commands" 5 | "github.com/stackus/ftgogo/customer-web/internal/application/queries" 6 | ) 7 | 8 | type Service struct { 9 | Commands Commands 10 | Queries Queries 11 | } 12 | 13 | type Commands struct { 14 | RegisterConsumer commands.RegisterConsumerHandler 15 | CreateOrder commands.CreateOrderHandler 16 | ReviseOrder commands.ReviseOrderHandler 17 | CancelOrder commands.CancelOrderHandler 18 | AddConsumerAddress commands.AddConsumerAddressHandler 19 | UpdateConsumerAddress commands.UpdateConsumerAddressHandler 20 | RemoveConsumerAddress commands.RemoveConsumerAddressHandler 21 | } 22 | 23 | type Queries struct { 24 | GetConsumer queries.GetConsumerHandler 25 | GetOrder queries.GetOrderHandler 26 | GetConsumerAddress queries.GetConsumerAddressHandler 27 | SearchOrders queries.SearchOrdersHandler 28 | } 29 | -------------------------------------------------------------------------------- /customer-web/internal/domain/consumer.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Consumer struct { 4 | ConsumerID string 5 | Name string 6 | // TODO Addresses map[string]consumerapi.Address ?? 7 | } 8 | -------------------------------------------------------------------------------- /customer-web/internal/domain/order.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/serviceapis/orderapi" 5 | ) 6 | 7 | type Order struct { 8 | OrderID string 9 | ConsumerID string 10 | RestaurantID string 11 | RestaurantName string 12 | Total int 13 | Status orderapi.OrderState 14 | // TODO EstimatedDelivery time.Duration 15 | // TODO other data 16 | } 17 | -------------------------------------------------------------------------------- /customer-web/internal/domain/order_history.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/orderapi" 7 | ) 8 | 9 | type OrderHistory struct { 10 | OrderID string 11 | ConsumerID string 12 | RestaurantID string 13 | RestaurantName string 14 | Status orderapi.OrderState 15 | CreatedAt time.Time 16 | } 17 | 18 | type SearchOrdersFilters struct { 19 | Keywords []string 20 | Since time.Time 21 | Status orderapi.OrderState 22 | } 23 | -------------------------------------------------------------------------------- /customer-web/internal/handlers/oapi-codegen.cfg.yaml: -------------------------------------------------------------------------------- 1 | output: web_server_api.gen.go 2 | package: handlers 3 | generate: 4 | - types 5 | - chi-server 6 | - spec 7 | import-mapping: 8 | ../../../serviceapis/commonapi/spec.yaml: github.com/stackus/ftgogo/serviceapis/commonapi 9 | -------------------------------------------------------------------------------- /delivery/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=delivery-service -------------------------------------------------------------------------------- /delivery/cmd/service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/delivery/internal/adapters" 5 | "github.com/stackus/ftgogo/delivery/internal/application" 6 | "github.com/stackus/ftgogo/delivery/internal/handlers" 7 | "github.com/stackus/ftgogo/serviceapis" 8 | "shared-go/applications" 9 | ) 10 | 11 | func main() { 12 | svc := applications.NewService(initService) 13 | if err := svc.Execute(); err != nil { 14 | panic(err) 15 | } 16 | } 17 | 18 | func initService(svc *applications.Service) error { 19 | serviceapis.RegisterTypes() 20 | 21 | // Driven 22 | courierRepo := adapters.NewCourierPostgresRepository(svc.PgConn) 23 | deliveryRepo := adapters.NewDeliveryPostgresRepository(svc.PgConn) 24 | restaurantRepo := adapters.NewRestaurantPostgresRepository(svc.PgConn) 25 | 26 | app := application.NewServiceApplication(courierRepo, deliveryRepo, restaurantRepo) 27 | 28 | // Drivers 29 | handlers.NewRestaurantEventHandlers(app).Mount(svc.Subscriber) 30 | handlers.NewOrderEventHandlers(app).Mount(svc.Subscriber) 31 | handlers.NewTicketEventHandlers(app).Mount(svc.Subscriber) 32 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /delivery/delvmod/setup.go: -------------------------------------------------------------------------------- 1 | package delvmod 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/delivery/internal/adapters" 5 | "github.com/stackus/ftgogo/delivery/internal/application" 6 | "github.com/stackus/ftgogo/delivery/internal/handlers" 7 | "shared-go/applications" 8 | ) 9 | 10 | func Setup(svc *applications.Monolith) error { 11 | // Driven 12 | adapters.CouriersTableName = "delivery.couriers" 13 | courierRepo := adapters.NewCourierPostgresRepository(svc.PgConn) 14 | adapters.DeliveriesTableName = "delivery.deliveries" 15 | deliveryRepo := adapters.NewDeliveryPostgresRepository(svc.PgConn) 16 | adapters.RestaurantsTableName = "delivery.restaurants" 17 | restaurantRepo := adapters.NewRestaurantPostgresRepository(svc.PgConn) 18 | 19 | app := application.NewServiceApplication(courierRepo, deliveryRepo, restaurantRepo) 20 | 21 | // Drivers 22 | handlers.NewRestaurantEventHandlers(app).Mount(svc.Subscriber) 23 | handlers.NewOrderEventHandlers(app).Mount(svc.Subscriber) 24 | handlers.NewTicketEventHandlers(app).Mount(svc.Subscriber) 25 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /delivery/features/cancel_delivery.feature: -------------------------------------------------------------------------------- 1 | @command @delivery 2 | Feature: Cancel Deliveries 3 | 4 | Background: Setup resources 5 | Given a restaurant named "Best Foods" exists with address 6 | | Street1 | 123 Address St. | 7 | | City | BigTown | 8 | | State | Tristate | 9 | | Zip | 90210 | 10 | And I create a delivery for order "A123" from "Best Foods" to address 11 | | Street1 | 123 Address St. | 12 | | City | BigTown | 13 | | State | Tristate | 14 | | Zip | 90210 | 15 | 16 | 17 | Scenario: Cancel existing deliveries 18 | When I cancel delivery for order "A123" 19 | Then I expect the command to succeed 20 | 21 | Scenario: Canceling deliveries that do not exist returns an error 22 | When I cancel delivery for order "B456" 23 | Then I expect the command to fail 24 | And the returned error message is "delivery not found" 25 | -------------------------------------------------------------------------------- /delivery/features/create_restaurant.feature: -------------------------------------------------------------------------------- 1 | @command @restaurant 2 | Feature: Create Restaurants 3 | 4 | Scenario: Can create new restaurants 5 | When I create a restaurant named "Best Foods" with address 6 | | Street1 | 123 Address St. | 7 | | City | BigTown | 8 | | State | Tristate | 9 | | Zip | 90210 | 10 | Then I expect the command to succeed 11 | 12 | Scenario: Creating duplicate restaurants returns an error 13 | Given I create a restaurant named "Best Foods" with address 14 | | Street1 | 123 Address St. | 15 | | City | BigTown | 16 | | State | Tristate | 17 | | Zip | 90210 | 18 | When I create another restaurant named "Best Foods" with address 19 | | Street1 | 123 Address St. | 20 | | City | BigTown | 21 | | State | Tristate | 22 | | Zip | 90210 | 23 | Then I expect the command to fail 24 | And the returned error message is "restaurant already exists" 25 | -------------------------------------------------------------------------------- /delivery/features/get_courier.feature: -------------------------------------------------------------------------------- 1 | @query @courier 2 | Feature: Get Couriers 3 | 4 | Background: Setup a Courier 5 | Given a courier exists named "Quick Courier" 6 | 7 | Scenario: Can get couriers 8 | When I get the courier named "Quick Courier" 9 | Then I expect the request to succeed 10 | And the returned courier is available 11 | 12 | Scenario: Can get unavailable couriers 13 | Given I set the courier "Quick Courier" to be unavailable 14 | When I get the courier named "Quick Courier" 15 | Then I expect the request to succeed 16 | And the returned courier is not available 17 | -------------------------------------------------------------------------------- /delivery/features/get_delivery.feature: -------------------------------------------------------------------------------- 1 | @query @delivery 2 | Feature: Get Deliveries 3 | 4 | Background: Setup resources 5 | Given a restaurant named "Best Foods" exists with address 6 | | Street1 | 123 Address St. | 7 | | City | BigTown | 8 | | State | Tristate | 9 | | Zip | 90210 | 10 | And I create a delivery for order "A123" from "Best Foods" to address 11 | | Street1 | 123 Address St. | 12 | | City | BigTown | 13 | | State | Tristate | 14 | | Zip | 90210 | 15 | 16 | Scenario: Can get deliveries 17 | When I get the delivery information for order "A123" 18 | Then I expect the request to succeed 19 | 20 | 21 | Scenario: Requesting deliveries for orders that do not exist returns an error 22 | When I get the delivery information for order "B456" 23 | Then I expect the request to fail 24 | And the returned error message is "delivery not found" 25 | -------------------------------------------------------------------------------- /delivery/features/set_courier_availability.feature: -------------------------------------------------------------------------------- 1 | @command @courier 2 | Feature: Setting Courier Availability 3 | 4 | Scenario: Couriers can be created 5 | When I set the courier "Quick Courier" to be available 6 | Then I expect the command to succeed 7 | -------------------------------------------------------------------------------- /delivery/features/steps/cancel_delivery.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/delivery/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterCancelDeliverySteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I cancel delivery for order "([^"]*)"$`, f.iCancelDeliveryForOrder) 13 | } 14 | 15 | func (f *FeatureState) iCancelDeliveryForOrder(orderID string) error { 16 | f.err = f.app.CancelDelivery(context.Background(), commands.CancelDelivery{OrderID: orderID}) 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /delivery/features/steps/create_delivery.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/delivery/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterCreateDeliverySteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I create (?:a|another) delivery for order "([^"]*)" from "([^"]*)" to address$`, f.iCreateADeliveryForOrderFromToAddress) 13 | } 14 | 15 | func (f *FeatureState) iCreateADeliveryForOrderFromToAddress(orderID, restaurantName string, table *godog.Table) error { 16 | address, err := parseAddressFromTable(table) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | restaurantID := f.restaurantIDs[restaurantName] 22 | 23 | f.err = f.app.CreateDelivery(context.Background(), commands.CreateDelivery{ 24 | OrderID: orderID, 25 | RestaurantID: restaurantID, 26 | DeliveryAddress: address, 27 | }) 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /delivery/features/steps/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | "github.com/google/uuid" 8 | 9 | "github.com/stackus/ftgogo/delivery/internal/application/commands" 10 | ) 11 | 12 | func (f *FeatureState) RegisterCreateRestaurantSteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^(?:I create )?(?:a|another) restaurant named "([^"]*)" (?:exists )?with address$`, f.aRestaurantNamedExistsWithAddress) 14 | } 15 | 16 | func (f *FeatureState) aRestaurantNamedExistsWithAddress(restaurantName string, table *godog.Table) error { 17 | address, err := parseAddressFromTable(table) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | restaurantID := f.restaurantIDs[restaurantName] 23 | if restaurantID == "" { 24 | restaurantID = uuid.New().String() 25 | f.restaurantIDs[restaurantName] = restaurantID 26 | } 27 | 28 | f.err = f.app.CreateRestaurant(context.Background(), commands.CreateRestaurant{ 29 | RestaurantID: restaurantID, 30 | Name: restaurantName, 31 | Address: address, 32 | }) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /delivery/features/steps/schedule_delivery.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/cucumber/godog" 8 | 9 | "github.com/stackus/ftgogo/delivery/internal/application/commands" 10 | ) 11 | 12 | func (f *FeatureState) RegisterScheduleDeliverySteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^I schedule the delivery for order "([^"]*)"$`, f.iScheduleTheDeliveryForOrder) 14 | } 15 | 16 | func (f *FeatureState) iScheduleTheDeliveryForOrder(orderID string) error { 17 | f.err = f.app.ScheduleDelivery(context.Background(), commands.ScheduleDelivery{ 18 | OrderID: orderID, 19 | ReadyBy: time.Now(), 20 | }) 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /delivery/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/delivery 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/cucumber/godog v0.11.0 16 | github.com/google/uuid v1.3.0 17 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 18 | github.com/hashicorp/go-memdb v1.3.2 // indirect 19 | github.com/mattn/go-colorable v0.1.8 // indirect 20 | github.com/rdumont/assistdog v0.0.0-20201106100018-168b06230d14 21 | github.com/spf13/pflag v1.0.5 22 | github.com/stackus/edat v0.0.6 23 | github.com/stackus/edat-msgpack v0.0.2 24 | github.com/stackus/edat-pgx v0.0.2 25 | github.com/stackus/errors v0.0.3 26 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 27 | google.golang.org/grpc v1.39.0 28 | google.golang.org/protobuf v1.27.1 29 | shared-go v0.0.0-00010101000000-000000000000 30 | ) 31 | -------------------------------------------------------------------------------- /delivery/internal/application/commands/cancel_delivery.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/application/ports" 7 | ) 8 | 9 | type CancelDelivery struct { 10 | OrderID string 11 | } 12 | 13 | type CancelDeliveryHandler struct { 14 | deliveryRepo ports.DeliveryRepository 15 | courierRepo ports.CourierRepository 16 | } 17 | 18 | func NewCancelDeliveryHandler(deliveryRepo ports.DeliveryRepository, courierRepo ports.CourierRepository) CancelDeliveryHandler { 19 | return CancelDeliveryHandler{ 20 | deliveryRepo: deliveryRepo, 21 | courierRepo: courierRepo, 22 | } 23 | } 24 | 25 | func (h CancelDeliveryHandler) Handle(ctx context.Context, cmd CancelDelivery) error { 26 | delivery, err := h.deliveryRepo.Find(ctx, cmd.OrderID) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if delivery.AssignedCourierID != "" { 32 | courier, err := h.courierRepo.Find(ctx, delivery.AssignedCourierID) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | courier.CancelDelivery(delivery.DeliveryID) 38 | 39 | err = h.courierRepo.Update(ctx, courier.CourierID, courier) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | delivery.Cancel() 46 | 47 | return h.deliveryRepo.Update(ctx, delivery.DeliveryID, delivery) 48 | } 49 | -------------------------------------------------------------------------------- /delivery/internal/application/commands/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/application/ports" 7 | "github.com/stackus/ftgogo/delivery/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | ) 10 | 11 | type CreateRestaurant struct { 12 | RestaurantID string 13 | Name string 14 | Address *commonapi.Address 15 | } 16 | 17 | type CreateRestaurantHandler struct { 18 | repo ports.RestaurantRepository 19 | } 20 | 21 | func NewCreateRestaurantHandler(restaurantRepo ports.RestaurantRepository) CreateRestaurantHandler { 22 | return CreateRestaurantHandler{repo: restaurantRepo} 23 | } 24 | 25 | func (h CreateRestaurantHandler) Handle(ctx context.Context, cmd CreateRestaurant) error { 26 | return h.repo.Save(ctx, &domain.Restaurant{ 27 | RestaurantID: cmd.RestaurantID, 28 | Name: cmd.Name, 29 | Address: cmd.Address, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /delivery/internal/application/commands/set_courier_availability.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/application/ports" 7 | ) 8 | 9 | type SetCourierAvailability struct { 10 | CourierID string 11 | Available bool 12 | } 13 | 14 | type SetCourierAvailabilityHandler struct { 15 | repo ports.CourierRepository 16 | } 17 | 18 | func NewSetCourierAvailabilityHandler(courierRepo ports.CourierRepository) SetCourierAvailabilityHandler { 19 | return SetCourierAvailabilityHandler{repo: courierRepo} 20 | } 21 | 22 | func (h SetCourierAvailabilityHandler) Handle(ctx context.Context, cmd SetCourierAvailability) error { 23 | courier, err := h.repo.FindOrCreate(ctx, cmd.CourierID) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | courier.Available = cmd.Available 29 | 30 | return h.repo.Update(ctx, cmd.CourierID, courier) 31 | } 32 | -------------------------------------------------------------------------------- /delivery/internal/application/ports/courier_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/domain" 7 | ) 8 | 9 | type CourierRepository interface { 10 | Find(ctx context.Context, courierID string) (*domain.Courier, error) 11 | FindOrCreate(ctx context.Context, courierID string) (*domain.Courier, error) 12 | FindFirstAvailable(ctx context.Context) (*domain.Courier, error) 13 | Save(ctx context.Context, courier *domain.Courier) error 14 | Update(ctx context.Context, courierID string, courier *domain.Courier) error 15 | } 16 | -------------------------------------------------------------------------------- /delivery/internal/application/ports/delivery_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/domain" 7 | ) 8 | 9 | type DeliveryRepository interface { 10 | Find(ctx context.Context, deliveryID string) (*domain.Delivery, error) 11 | Save(ctx context.Context, delivery *domain.Delivery) error 12 | Update(ctx context.Context, deliveryID string, delivery *domain.Delivery) error 13 | } 14 | -------------------------------------------------------------------------------- /delivery/internal/application/ports/restaurant_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/domain" 7 | ) 8 | 9 | type RestaurantRepository interface { 10 | Find(ctx context.Context, restaurantID string) (*domain.Restaurant, error) 11 | Save(ctx context.Context, restaurant *domain.Restaurant) error 12 | Update(ctx context.Context, restaurantID string, restaurant *domain.Restaurant) error 13 | } 14 | -------------------------------------------------------------------------------- /delivery/internal/application/queries/get_courier.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/application/ports" 7 | "github.com/stackus/ftgogo/delivery/internal/domain" 8 | ) 9 | 10 | type GetCourier struct { 11 | CourierID string 12 | } 13 | 14 | type GetCourierHandler struct { 15 | repo ports.CourierRepository 16 | } 17 | 18 | func NewGetCourierHandler(repo ports.CourierRepository) GetCourierHandler { 19 | return GetCourierHandler{repo: repo} 20 | } 21 | 22 | func (h GetCourierHandler) Handle(ctx context.Context, query GetCourier) (*domain.Courier, error) { 23 | return h.repo.Find(ctx, query.CourierID) 24 | } 25 | -------------------------------------------------------------------------------- /delivery/internal/application/queries/get_delivery.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/delivery/internal/application/ports" 7 | "github.com/stackus/ftgogo/delivery/internal/domain" 8 | ) 9 | 10 | type GetDelivery struct { 11 | OrderID string 12 | } 13 | 14 | type GetDeliveryHandler struct { 15 | repo ports.DeliveryRepository 16 | } 17 | 18 | func NewGetDeliveryHandler(repo ports.DeliveryRepository) GetDeliveryHandler { 19 | return GetDeliveryHandler{ 20 | repo: repo, 21 | } 22 | } 23 | 24 | func (h GetDeliveryHandler) Handle(ctx context.Context, query GetDelivery) (*domain.Delivery, error) { 25 | return h.repo.Find(ctx, query.OrderID) 26 | } 27 | -------------------------------------------------------------------------------- /delivery/internal/domain/delivery.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/errors" 7 | 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | ) 10 | 11 | type DeliveryStatus int 12 | 13 | const ( 14 | DeliveryPending DeliveryStatus = iota 15 | DeliveryScheduled 16 | DeliveryCancelled 17 | ) 18 | 19 | type Delivery struct { 20 | DeliveryID string 21 | RestaurantID string 22 | AssignedCourierID string 23 | PickUpAddress *commonapi.Address 24 | DeliveryAddress *commonapi.Address 25 | Status DeliveryStatus 26 | PickUpTime time.Time 27 | ReadyBy time.Time 28 | } 29 | 30 | // Delivery errors 31 | var ( 32 | ErrDeliveryNotFound = errors.Wrap(errors.ErrNotFound, "delivery not found") 33 | ) 34 | 35 | func (s DeliveryStatus) String() string { 36 | switch s { 37 | case DeliveryPending: 38 | return "PENDING" 39 | case DeliveryScheduled: 40 | return "SCHEDULED" 41 | case DeliveryCancelled: 42 | return "CANCELLED" 43 | } 44 | 45 | return "UNKNOWN" 46 | } 47 | 48 | func (d *Delivery) Schedule(readyBy time.Time, assignedCourierID string) { 49 | d.ReadyBy = readyBy 50 | d.AssignedCourierID = assignedCourierID 51 | d.Status = DeliveryScheduled 52 | } 53 | 54 | func (d *Delivery) Cancel() { 55 | d.AssignedCourierID = "" 56 | d.Status = DeliveryCancelled 57 | } 58 | -------------------------------------------------------------------------------- /delivery/internal/domain/restaurant.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/errors" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | ) 8 | 9 | // Restaurant errors 10 | var ( 11 | ErrRestaurantNotFound = errors.Wrap(errors.ErrNotFound, "restaurant not found") 12 | ) 13 | 14 | type Restaurant struct { 15 | RestaurantID string 16 | Name string 17 | Address *commonapi.Address 18 | // note: no menu items 19 | } 20 | -------------------------------------------------------------------------------- /delivery/internal/handlers/order_event_handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/msg" 7 | 8 | "github.com/stackus/ftgogo/delivery/internal/application" 9 | "github.com/stackus/ftgogo/delivery/internal/application/commands" 10 | "github.com/stackus/ftgogo/serviceapis/commonapi" 11 | "github.com/stackus/ftgogo/serviceapis/orderapi" 12 | ) 13 | 14 | type OrderEventHandlers struct { 15 | app application.ServiceApplication 16 | } 17 | 18 | func NewOrderEventHandlers(app application.ServiceApplication) OrderEventHandlers { 19 | return OrderEventHandlers{app: app} 20 | } 21 | 22 | func (h OrderEventHandlers) Mount(subscriber *msg.Subscriber) { 23 | subscriber.Subscribe(orderapi.OrderAggregateChannel, msg.NewEntityEventDispatcher(). 24 | Handle(orderapi.OrderCreated{}, h.OrderCreated)) 25 | } 26 | 27 | func (h OrderEventHandlers) OrderCreated(ctx context.Context, evtMsg msg.EntityEvent) error { 28 | evt := evtMsg.Event().(*orderapi.OrderCreated) 29 | 30 | return h.app.CreateDelivery(ctx, commands.CreateDelivery{ 31 | OrderID: evtMsg.EntityID(), 32 | RestaurantID: evt.RestaurantID, 33 | DeliveryAddress: &commonapi.Address{ 34 | Street1: evt.DeliverTo.Street1, 35 | Street2: evt.DeliverTo.Street2, 36 | City: evt.DeliverTo.City, 37 | State: evt.DeliverTo.State, 38 | Zip: evt.DeliverTo.Zip, 39 | }, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /deployment/kubernetes/config-map.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: common-config 5 | namespace: ftgogo 6 | data: 7 | GET_HOSTS_FROM: dns 8 | ENVIRONMENT: development 9 | LOG_LEVEL: TRACE 10 | EVENT_DRIVER: nats 11 | NATS_URL: stan:4222 12 | NATS_CLUSTER_ID: test-cluster 13 | -------------------------------------------------------------------------------- /deployment/kubernetes/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: ftgogo 5 | -------------------------------------------------------------------------------- /docker-compose-monolith.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | x-environment: &default-environment 3 | GET_HOSTS_FROM: dns 4 | ENVIRONMENT: development 5 | LOG_LEVEL: TRACE 6 | EVENT_DRIVER: inmem 7 | 8 | services: 9 | postgres: 10 | container_name: postgres 11 | hostname: postgres 12 | image: postgres:alpine 13 | restart: always 14 | environment: 15 | GET_HOST_FROM: dns 16 | POSTGRES_PASSWORD: itsasecret 17 | networks: 18 | - mononet 19 | ports: 20 | - '5432:5432' 21 | volumes: 22 | - 'pgdata:/var/lib/postgresql/data' 23 | - './config/postgresql/init-postgres.sql:/docker-entrypoint-initdb.d/init-postgres.sql' 24 | 25 | monolith-service: 26 | hostname: monolith 27 | restart: on-failure 28 | build: 29 | context: . 30 | args: 31 | service: monolith 32 | environment: 33 | <<: *default-environment 34 | SERVICE_ID: monolith-service 35 | PG_CONN: host=postgres dbname=ftgogo user=ftgogo_user password=ftgogo_pass pool_max_conns=10 36 | RPC_SERVER_NETWORK: unix 37 | RPC_SERVER_ADDRESS: /tmp/grpc.sock 38 | networks: 39 | - mononet 40 | ports: 41 | - '8000:80' 42 | 43 | networks: 44 | mononet: 45 | 46 | volumes: 47 | consuldata: 48 | pgdata: 49 | standata: 50 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackus/ftgogo/c5028ec7edb09749f234ff4086b0e25e1cedca4d/docs/architecture.png -------------------------------------------------------------------------------- /docs/cancelOrderSaga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackus/ftgogo/c5028ec7edb09749f234ff4086b0e25e1cedca4d/docs/cancelOrderSaga.png -------------------------------------------------------------------------------- /docs/createOrderSaga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackus/ftgogo/c5028ec7edb09749f234ff4086b0e25e1cedca4d/docs/createOrderSaga.png -------------------------------------------------------------------------------- /docs/hexagonal_architecture_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackus/ftgogo/c5028ec7edb09749f234ff4086b0e25e1cedca4d/docs/hexagonal_architecture_w.png -------------------------------------------------------------------------------- /docs/outbox_pattern_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackus/ftgogo/c5028ec7edb09749f234ff4086b0e25e1cedca4d/docs/outbox_pattern_bg.png -------------------------------------------------------------------------------- /docs/reviseOrderSaga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackus/ftgogo/c5028ec7edb09749f234ff4086b0e25e1cedca4d/docs/reviseOrderSaga.png -------------------------------------------------------------------------------- /kitchen/cmd/cdc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "shared-go/applications" 5 | ) 6 | 7 | func main() { 8 | cdc := applications.NewCDC(func(*applications.CDC) error { return nil }) 9 | if err := cdc.Execute(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /kitchen/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=kitchen-service -------------------------------------------------------------------------------- /kitchen/cmd/service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/kitchen/internal/adapters" 5 | "github.com/stackus/ftgogo/kitchen/internal/application" 6 | "github.com/stackus/ftgogo/kitchen/internal/domain" 7 | "github.com/stackus/ftgogo/kitchen/internal/handlers" 8 | "github.com/stackus/ftgogo/serviceapis" 9 | "shared-go/applications" 10 | ) 11 | 12 | func main() { 13 | svc := applications.NewService(initService) 14 | if err := svc.Execute(); err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | func initService(svc *applications.Service) error { 20 | serviceapis.RegisterTypes() 21 | domain.RegisterTypes() 22 | 23 | // Driven 24 | ticketRepo := adapters.NewTicketRepositoryPublisherMiddleware( 25 | adapters.NewTicketAggregateRepository(svc.AggregateStore), 26 | adapters.NewTicketEntityEventPublisher(svc.Publisher), 27 | ) 28 | restaurantRepo := adapters.NewRestaurantPostgresRepository(svc.PgConn) 29 | 30 | app := application.NewServiceApplication(ticketRepo, restaurantRepo) 31 | 32 | // Drivers 33 | handlers.NewCommandHandlers(app).Mount(svc.Subscriber, svc.Publisher) 34 | handlers.NewRestaurantEventHandlers(app).Mount(svc.Subscriber) 35 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /kitchen/feature_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/cucumber/godog" 8 | flag "github.com/spf13/pflag" 9 | 10 | "github.com/stackus/ftgogo/kitchen/features/steps" 11 | ) 12 | 13 | var opts = godog.Options{ 14 | Format: "progress", 15 | NoColors: true, 16 | } 17 | 18 | func init() { 19 | godog.BindCommandLineFlags("godog.", &opts) 20 | } 21 | 22 | func InitializeScenario(ctx *godog.ScenarioContext) { 23 | state := steps.NewFeatureState() 24 | 25 | ctx.BeforeScenario(func(*godog.Scenario) { 26 | state.Reset() 27 | }) 28 | 29 | state.RegisterCommonSteps(ctx) 30 | state.RegisterCreateTicketSteps(ctx) 31 | state.RegisterCancelTicketSteps(ctx) 32 | state.RegisterReviseTicketSteps(ctx) 33 | state.RegisterAcceptTicketSteps(ctx) 34 | state.RegisterCreateRestaurantSteps(ctx) 35 | state.RegisterCreateRestaurantSteps(ctx) 36 | state.RegisterGetTicketSteps(ctx) 37 | } 38 | 39 | func TestMain(m *testing.M) { 40 | flag.Parse() 41 | for _, arg := range os.Args[1:] { 42 | if arg == "-test.v=true" { 43 | opts.Format = "pretty" 44 | break 45 | } 46 | } 47 | 48 | status := godog.TestSuite{ 49 | Name: "kitchen features", 50 | ScenarioInitializer: InitializeScenario, 51 | Options: &opts, 52 | }.Run() 53 | 54 | if st := m.Run(); st != 0 { 55 | os.Exit(st) 56 | } 57 | 58 | if status != 0 { 59 | os.Exit(status) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /kitchen/features/accept_ticket.feature: -------------------------------------------------------------------------------- 1 | @command @ticket 2 | Feature: Accept Tickets 3 | 4 | Background: Setup Ticket 5 | Given I have created a ticket for order "A123" at restaurant "Best Foods" with items 6 | | MenuItemID | Name | Quantity | 7 | | I123 | Yummy Dish | 1 | 8 | And I have confirmed creating a ticket for order "A123" 9 | 10 | Scenario: Confirmed tickets can be accepted 11 | When I accept the ticket for order "A123" will be ready in 30 minutes 12 | Then I expect the command to succeed 13 | 14 | Scenario: Accepted tickets have the status "Accepted" 15 | Given I accept the ticket for order "A123" will be ready in 30 minutes 16 | When I get the ticket for order "A123" 17 | Then I expect the command to succeed 18 | And the returned ticket status is "Accepted" 19 | -------------------------------------------------------------------------------- /kitchen/features/cancel_ticket.feature: -------------------------------------------------------------------------------- 1 | @command @ticket @cancel 2 | Feature: Cancelling Tickets 3 | 4 | Background: Setup Ticket 5 | Given I have created a ticket for order "A123" at restaurant "Best Foods" with items 6 | | MenuItemID | Name | Quantity | 7 | | I123 | Yummy Dish | 1 | 8 | And I have confirmed creating a ticket for order "A123" 9 | And I have accepted the ticket for order "A123" will be ready in 30 minutes 10 | 11 | Scenario: Accepted tickets can be cancelled 12 | When I begin cancelling the ticket for order "A123" 13 | Then I expect the command to succeed 14 | 15 | Scenario: Tickets can be fully cancelled 16 | Given I have begun cancelling the ticket for order "A123" 17 | When I confirm cancelling the ticket for order "A123" 18 | Then I expect the command to succeed 19 | 20 | Scenario: Tickets being cancelled cannot be revised 21 | Given I have begun cancelling the ticket for order "A123" 22 | When I begin revising the ticket for order "A123" 23 | Then I expect the command to fail 24 | And the returned error message is "ticket state does not allow action" 25 | -------------------------------------------------------------------------------- /kitchen/features/get_ticket.feature: -------------------------------------------------------------------------------- 1 | @query @ticket 2 | Feature: Get Tickets 3 | 4 | Background: Setup resources 5 | Given I have created a ticket for order "A123" and restaurant "Best Foods" with items 6 | | MenuItemID | Name | Quantity | 7 | | I123 | Yummy Dish | 1 | 8 | 9 | Scenario: Can get tickets 10 | When I get the ticket for order "A123" 11 | Then I expect the request to succeed 12 | 13 | 14 | Scenario: Requesting tickets that do not exist returns an error 15 | When I get the ticket for order "B456" 16 | Then I expect the request to fail 17 | And the returned error message is "ticket not found" 18 | -------------------------------------------------------------------------------- /kitchen/features/revise_ticket.feature: -------------------------------------------------------------------------------- 1 | @command @ticket @revise 2 | Feature: Revising Tickets 3 | 4 | Background: Setup Ticket 5 | Given I have created a ticket for order "A123" and restaurant "Best Foods" with items 6 | | MenuItemID | Name | Quantity | 7 | | I123 | Yummy Dish | 1 | 8 | And I have confirmed creating a ticket for order "A123" 9 | And I have accepted the ticket for order "A123" will be ready in 30 minutes 10 | 11 | Scenario: Accepted tickets can be revised 12 | When I begin revising the ticket for order "A123" 13 | Then I expect the command to succeed 14 | 15 | Scenario: Tickets can be fully revised 16 | Given I have begun revising the ticket for order "A123" 17 | When I confirm revising the ticket for order "A123" 18 | Then I expect the command to succeed 19 | 20 | Scenario: Tickets being revised cannot be cancelled 21 | Given I have begun revising the ticket for order "A123" 22 | When I begin cancelling the ticket for order "A123" 23 | Then I expect the command to fail 24 | And the returned error message is "ticket state does not allow action" 25 | -------------------------------------------------------------------------------- /kitchen/features/steps/accept_ticket.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/cucumber/godog" 8 | 9 | "github.com/stackus/ftgogo/kitchen/internal/application/commands" 10 | ) 11 | 12 | func (f *FeatureState) RegisterAcceptTicketSteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^I (?:accept|have accepted) (?:a|the|another) ticket for order "([^"]*)" will be ready in (\d+) minutes$`, f.iAcceptThatTicketWillBeReadyInMinutesForOrder) 14 | } 15 | 16 | func (f *FeatureState) iAcceptThatTicketWillBeReadyInMinutesForOrder(orderID string, minutes int) error { 17 | ticketID := f.ticketIDs[orderID] 18 | 19 | f.err = f.app.AcceptTicket(context.Background(), commands.AcceptTicket{ 20 | TicketID: ticketID, 21 | ReadyBy: time.Now().Add(time.Minute * time.Duration(minutes)), 22 | }) 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /kitchen/features/steps/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/cucumber/godog" 8 | 9 | "github.com/stackus/ftgogo/kitchen/internal/application/commands" 10 | ) 11 | 12 | func (f *FeatureState) RegisterCreateRestaurantSteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^I (?:create|setup) (?:a|the|another) restaurant with:$`, f.iCreateARestaurantWith) 14 | } 15 | 16 | func (f *FeatureState) iCreateARestaurantWith(doc *godog.DocString) error { 17 | var cmd commands.CreateRestaurant 18 | 19 | err := json.Unmarshal([]byte(doc.Content), &cmd) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | f.err = f.app.CreateRestaurant(context.Background(), cmd) 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /kitchen/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/kitchen 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/cucumber/godog v0.11.0 16 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 17 | github.com/hashicorp/go-memdb v1.3.2 // indirect 18 | github.com/mattn/go-colorable v0.1.8 // indirect 19 | github.com/rdumont/assistdog v0.0.0-20201106100018-168b06230d14 20 | github.com/spf13/pflag v1.0.5 21 | github.com/stackus/edat v0.0.6 22 | github.com/stackus/edat-msgpack v0.0.2 23 | github.com/stackus/edat-pgx v0.0.2 24 | github.com/stackus/errors v0.0.3 25 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 26 | google.golang.org/grpc v1.39.0 27 | shared-go v0.0.0-00010101000000-000000000000 28 | ) 29 | -------------------------------------------------------------------------------- /kitchen/internal/adapters/ticket_entity_event_publisher.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | ) 8 | 9 | func NewTicketEntityEventPublisher(publisher msg.EntityEventMessagePublisher) ports.TicketPublisher { 10 | return publisher 11 | } 12 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/accept_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 8 | "github.com/stackus/ftgogo/kitchen/internal/domain" 9 | ) 10 | 11 | type AcceptTicket struct { 12 | TicketID string 13 | ReadyBy time.Time 14 | } 15 | 16 | type AcceptTicketHandler struct { 17 | repo ports.TicketRepository 18 | } 19 | 20 | func NewAcceptTicketHandler(repo ports.TicketRepository) AcceptTicketHandler { 21 | return AcceptTicketHandler{ 22 | repo: repo, 23 | } 24 | } 25 | 26 | func (h AcceptTicketHandler) Handle(ctx context.Context, cmd AcceptTicket) error { 27 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.AcceptTicket{ReadyBy: cmd.ReadyBy}) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/begin_cancel_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type BeginCancelTicket struct { 11 | TicketID string 12 | } 13 | 14 | type BeginCancelTicketHandler struct { 15 | repo ports.TicketRepository 16 | } 17 | 18 | func NewBeginCancelTicketHandler(ticketRepo ports.TicketRepository) BeginCancelTicketHandler { 19 | return BeginCancelTicketHandler{repo: ticketRepo} 20 | } 21 | 22 | func (h BeginCancelTicketHandler) Handle(ctx context.Context, cmd BeginCancelTicket) error { 23 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.CancelTicket{}) 24 | 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/begin_revise_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type BeginReviseTicket struct { 11 | TicketID string 12 | } 13 | 14 | type BeginReviseTicketHandler struct { 15 | repo ports.TicketRepository 16 | } 17 | 18 | func NewBeginReviseTicketHandler(ticketRepo ports.TicketRepository) BeginReviseTicketHandler { 19 | return BeginReviseTicketHandler{repo: ticketRepo} 20 | } 21 | 22 | func (h BeginReviseTicketHandler) Handle(ctx context.Context, cmd BeginReviseTicket) error { 23 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.ReviseTicket{}) 24 | 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/cancel_create_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type CancelCreateTicket struct { 11 | TicketID string 12 | } 13 | 14 | type CancelCreateTicketHandler struct { 15 | repo ports.TicketRepository 16 | } 17 | 18 | func NewCancelCreateTicketHandler(ticketRepo ports.TicketRepository) CancelCreateTicketHandler { 19 | return CancelCreateTicketHandler{ 20 | repo: ticketRepo, 21 | } 22 | } 23 | 24 | func (h CancelCreateTicketHandler) Handle(ctx context.Context, cmd CancelCreateTicket) error { 25 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.CancelCreateTicket{}) 26 | 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/confirm_cancel_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type ConfirmCancelTicket struct { 11 | TicketID string 12 | RestaurantID string 13 | } 14 | 15 | type ConfirmCancelTicketHandler struct { 16 | repo ports.TicketRepository 17 | } 18 | 19 | func NewConfirmCancelTicketHandler(repo ports.TicketRepository) ConfirmCancelTicketHandler { 20 | return ConfirmCancelTicketHandler{ 21 | repo: repo, 22 | } 23 | } 24 | 25 | func (h ConfirmCancelTicketHandler) Handle(ctx context.Context, cmd ConfirmCancelTicket) error { 26 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.ConfirmCancelTicket{}) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/confirm_create_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type ConfirmCreateTicket struct { 11 | TicketID string 12 | } 13 | 14 | type ConfirmCreateTicketHandler struct { 15 | repo ports.TicketRepository 16 | } 17 | 18 | func NewConfirmCreateTicketHandler(repo ports.TicketRepository) ConfirmCreateTicketHandler { 19 | return ConfirmCreateTicketHandler{ 20 | repo: repo, 21 | } 22 | } 23 | 24 | func (h ConfirmCreateTicketHandler) Handle(ctx context.Context, cmd ConfirmCreateTicket) error { 25 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.ConfirmCreateTicket{}) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/confirm_revise_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type ConfirmReviseTicket struct { 11 | TicketID string 12 | RestaurantID string 13 | RevisedQuantities map[string]int 14 | } 15 | 16 | type ConfirmReviseTicketHandler struct { 17 | repo ports.TicketRepository 18 | } 19 | 20 | func NewConfirmReviseTicketHandler(repo ports.TicketRepository) ConfirmReviseTicketHandler { 21 | return ConfirmReviseTicketHandler{ 22 | repo: repo, 23 | } 24 | } 25 | 26 | func (h ConfirmReviseTicketHandler) Handle(ctx context.Context, cmd ConfirmReviseTicket) error { 27 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.ConfirmReviseTicket{}) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 9 | ) 10 | 11 | type CreateRestaurant struct { 12 | RestaurantID string 13 | Name string 14 | Menu []restaurantapi.MenuItem 15 | } 16 | 17 | type CreateRestaurantHandler struct { 18 | repo ports.RestaurantRepository 19 | } 20 | 21 | func NewCreateRestaurantHandler(restaurantRepo ports.RestaurantRepository) CreateRestaurantHandler { 22 | return CreateRestaurantHandler{repo: restaurantRepo} 23 | } 24 | 25 | func (h CreateRestaurantHandler) Handle(ctx context.Context, cmd CreateRestaurant) error { 26 | return h.repo.Save(ctx, &domain.Restaurant{ 27 | RestaurantID: cmd.RestaurantID, 28 | Name: cmd.Name, 29 | MenuItems: cmd.Menu, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/create_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/kitchenapi" 9 | ) 10 | 11 | type CreateTicket struct { 12 | OrderID string 13 | RestaurantID string 14 | LineItems []kitchenapi.LineItem 15 | } 16 | 17 | type CreateTicketHandler struct { 18 | repo ports.TicketRepository 19 | } 20 | 21 | func NewCreateTicketHandler(ticketRepo ports.TicketRepository) CreateTicketHandler { 22 | return CreateTicketHandler{repo: ticketRepo} 23 | } 24 | 25 | func (h CreateTicketHandler) Handle(ctx context.Context, cmd CreateTicket) (string, error) { 26 | ticket, err := h.repo.Save(ctx, &domain.CreateTicket{ 27 | OrderID: cmd.OrderID, 28 | RestaurantID: cmd.RestaurantID, 29 | LineItems: cmd.LineItems, 30 | }) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | return ticket.ID(), nil 36 | } 37 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/revise_restaurant_menu.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 8 | ) 9 | 10 | type ReviseRestaurantMenu struct { 11 | RestaurantID string 12 | Menu []restaurantapi.MenuItem 13 | } 14 | 15 | type ReviseRestaurantMenuHandler struct { 16 | repo ports.RestaurantRepository 17 | } 18 | 19 | func NewReviseRestaurantMenuHandler(restaurantRepo ports.RestaurantRepository) ReviseRestaurantMenuHandler { 20 | return ReviseRestaurantMenuHandler{repo: restaurantRepo} 21 | } 22 | 23 | func (h ReviseRestaurantMenuHandler) Handle(ctx context.Context, cmd ReviseRestaurantMenu) error { 24 | restaurant, err := h.repo.Find(ctx, cmd.RestaurantID) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | err = restaurant.ReviseMenu(cmd.Menu) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = h.repo.Update(ctx, cmd.RestaurantID, restaurant) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/undo_cancel_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type UndoCancelTicket struct { 11 | TicketID string 12 | RestaurantID string 13 | } 14 | 15 | type UndoCancelTicketHandler struct { 16 | repo ports.TicketRepository 17 | } 18 | 19 | func NewUndoCancelTicketHandler(ticketRepo ports.TicketRepository) UndoCancelTicketHandler { 20 | return UndoCancelTicketHandler{repo: ticketRepo} 21 | } 22 | 23 | func (h UndoCancelTicketHandler) Handle(ctx context.Context, cmd UndoCancelTicket) error { 24 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.UndoCancelTicket{}) 25 | 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /kitchen/internal/application/commands/undo_revise_ticket.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type UndoReviseTicket struct { 11 | TicketID string 12 | RestaurantID string 13 | } 14 | 15 | type UndoReviseTicketHandler struct { 16 | repo ports.TicketRepository 17 | } 18 | 19 | func NewUndoReviseTicketHandler(ticketRepo ports.TicketRepository) UndoReviseTicketHandler { 20 | return UndoReviseTicketHandler{repo: ticketRepo} 21 | } 22 | 23 | func (h UndoReviseTicketHandler) Handle(ctx context.Context, cmd UndoReviseTicket) error { 24 | _, err := h.repo.Update(ctx, cmd.TicketID, &domain.UndoReviseTicket{}) 25 | 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /kitchen/internal/application/ports/restaurant_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/domain" 7 | ) 8 | 9 | type RestaurantRepository interface { 10 | Find(ctx context.Context, restaurantID string) (*domain.Restaurant, error) 11 | Save(ctx context.Context, restaurant *domain.Restaurant) error 12 | Update(ctx context.Context, restaurantID string, restaurant *domain.Restaurant) error 13 | } 14 | -------------------------------------------------------------------------------- /kitchen/internal/application/ports/ticket_publisher.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | ) 6 | 7 | type TicketPublisher interface { 8 | msg.EntityEventMessagePublisher 9 | } 10 | -------------------------------------------------------------------------------- /kitchen/internal/application/ports/ticket_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/es" 8 | 9 | "github.com/stackus/ftgogo/kitchen/internal/domain" 10 | ) 11 | 12 | type TicketRepository interface { 13 | Load(ctx context.Context, aggregateID string) (*domain.Ticket, error) 14 | Save(ctx context.Context, command core.Command, options ...es.AggregateRootOption) (*domain.Ticket, error) 15 | Update(ctx context.Context, aggregateID string, command core.Command, options ...es.AggregateRootOption) (*domain.Ticket, error) 16 | } 17 | -------------------------------------------------------------------------------- /kitchen/internal/application/queries/get_restaurant.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type GetRestaurant struct { 11 | RestaurantID string 12 | } 13 | 14 | type GetRestaurantHandler struct { 15 | repo ports.RestaurantRepository 16 | } 17 | 18 | func NewGetRestaurantHandler(restaurantRepo ports.RestaurantRepository) GetRestaurantHandler { 19 | return GetRestaurantHandler{repo: restaurantRepo} 20 | } 21 | 22 | func (h GetRestaurantHandler) Handle(ctx context.Context, query GetRestaurant) (*domain.Restaurant, error) { 23 | restaurant, err := h.repo.Find(ctx, query.RestaurantID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return restaurant, nil 29 | } 30 | -------------------------------------------------------------------------------- /kitchen/internal/application/queries/get_ticket.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/kitchen/internal/application/ports" 7 | "github.com/stackus/ftgogo/kitchen/internal/domain" 8 | ) 9 | 10 | type GetTicket struct { 11 | TicketID string 12 | } 13 | 14 | type GetTicketHandler struct { 15 | repo ports.TicketRepository 16 | } 17 | 18 | func NewGetTicketHandler(repo ports.TicketRepository) GetTicketHandler { 19 | return GetTicketHandler{repo: repo} 20 | } 21 | 22 | func (h GetTicketHandler) Handle(ctx context.Context, query GetTicket) (*domain.Ticket, error) { 23 | return h.repo.Load(ctx, query.TicketID) 24 | } 25 | -------------------------------------------------------------------------------- /kitchen/internal/domain/register_types.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | func RegisterTypes() { 4 | registerTicketCommands() 5 | registerTicketEvents() 6 | registerTicketSnapshots() 7 | } 8 | -------------------------------------------------------------------------------- /kitchen/internal/domain/restaurant.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/errors" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 7 | ) 8 | 9 | var ( 10 | ErrRestaurantNotFound = errors.Wrap(errors.ErrNotFound, "restaurant not found") 11 | ErrMenuItemNotFound = errors.Wrap(errors.ErrNotFound, "menu item not found") 12 | ) 13 | 14 | type Restaurant struct { 15 | RestaurantID string 16 | Name string 17 | MenuItems []restaurantapi.MenuItem 18 | } 19 | 20 | func (r *Restaurant) FindMenuItem(menuItemID string) (restaurantapi.MenuItem, error) { 21 | for _, item := range r.MenuItems { 22 | if menuItemID == item.ID { 23 | return item, nil 24 | } 25 | } 26 | 27 | return restaurantapi.MenuItem{}, ErrMenuItemNotFound 28 | } 29 | 30 | func (r *Restaurant) ReviseMenu([]restaurantapi.MenuItem) error { 31 | return errors.ErrUnimplemented 32 | } 33 | -------------------------------------------------------------------------------- /kitchen/internal/domain/ticket_snapshots.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/edat/core" 7 | 8 | "github.com/stackus/ftgogo/serviceapis/kitchenapi" 9 | ) 10 | 11 | func registerTicketSnapshots() { 12 | core.RegisterSnapshots(TicketSnapshot{}) 13 | } 14 | 15 | type TicketSnapshot struct { 16 | OrderID string 17 | RestaurantID string 18 | LineItems []kitchenapi.LineItem 19 | ReadyBy time.Time 20 | AcceptedAt time.Time 21 | PreparingTime time.Time 22 | ReadyForPickUpAt time.Time 23 | PickedUpAt time.Time 24 | State TicketState 25 | } 26 | 27 | func (TicketSnapshot) SnapshotName() string { return "kitchenservice.TicketSnapshot" } 28 | -------------------------------------------------------------------------------- /monolith/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=ftgogo-monolith 2 | -------------------------------------------------------------------------------- /order-history/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=order-history-service -------------------------------------------------------------------------------- /order-history/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/order-history 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/mattn/go-colorable v0.1.8 // indirect 16 | github.com/stackus/edat v0.0.6 17 | github.com/stackus/edat-pgx v0.0.2 18 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 19 | google.golang.org/grpc v1.39.0 20 | google.golang.org/protobuf v1.27.1 21 | shared-go v0.0.0-00010101000000-000000000000 22 | ) 23 | -------------------------------------------------------------------------------- /order-history/internal/adapters/order_history_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stackus/ftgogo/order-history/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/orderapi" 9 | ) 10 | 11 | type OrderHistoryFilters struct { 12 | Since time.Time // rely on the .IsZero() 13 | Keywords []string // ignored if empty 14 | Status orderapi.OrderState // no pointer necessary; zero value == Unknown 15 | Next string // ignored if empty 16 | Limit int // default to OrderHistoryLimit if not provided 17 | } 18 | 19 | // TODO update FindConsumerOrders to return a (*FindConsumerOrdersResult, error) pair 20 | 21 | type OrderHistoryRepository interface { 22 | FindConsumerOrders(ctx context.Context, consumerID string, filters OrderHistoryFilters) ([]*domain.OrderHistory, string, error) 23 | Find(ctx context.Context, orderHistoryID string) (*domain.OrderHistory, error) 24 | Save(ctx context.Context, orderHistory *domain.OrderHistory) error 25 | UpdateStatus(ctx context.Context, orderHistoryID string, status orderapi.OrderState) error 26 | Update(ctx context.Context, orderHistoryID string, orderHistory *domain.OrderHistory) error 27 | } 28 | -------------------------------------------------------------------------------- /order-history/internal/application/commands/update_order_status.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order-history/internal/adapters" 7 | "github.com/stackus/ftgogo/serviceapis/orderapi" 8 | ) 9 | 10 | type UpdateOrderStatus struct { 11 | OrderID string 12 | Status orderapi.OrderState 13 | } 14 | 15 | type UpdateOrderStatusHandler struct { 16 | repo adapters.OrderHistoryRepository 17 | } 18 | 19 | func NewUpdateOrderStatusHandler(orderHistoryRepo adapters.OrderHistoryRepository) UpdateOrderStatusHandler { 20 | return UpdateOrderStatusHandler{repo: orderHistoryRepo} 21 | } 22 | 23 | func (h UpdateOrderStatusHandler) Handle(ctx context.Context, cmd UpdateOrderStatus) error { 24 | return h.repo.UpdateStatus(ctx, cmd.OrderID, cmd.Status) 25 | } 26 | -------------------------------------------------------------------------------- /order-history/internal/application/queries/get_order_history.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order-history/internal/adapters" 7 | "github.com/stackus/ftgogo/order-history/internal/domain" 8 | ) 9 | 10 | type GetOrderHistory struct { 11 | OrderID string 12 | } 13 | 14 | type GetOrderHistoryHandler struct { 15 | repo adapters.OrderHistoryRepository 16 | } 17 | 18 | func NewGetOrderHistoryHandler(orderHistoryRepo adapters.OrderHistoryRepository) GetOrderHistoryHandler { 19 | return GetOrderHistoryHandler{repo: orderHistoryRepo} 20 | } 21 | 22 | func (h GetOrderHistoryHandler) Handle(ctx context.Context, query GetOrderHistory) (*domain.OrderHistory, error) { 23 | return h.repo.Find(ctx, query.OrderID) 24 | } 25 | -------------------------------------------------------------------------------- /order-history/internal/application/queries/spec.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | OrderHistory: 4 | type: object 5 | required: [ order_id, status, restaurant_id, restaurant_name ] 6 | properties: 7 | order_id: 8 | type: string 9 | format: uuid 10 | status: 11 | type: string 12 | restaurant_id: 13 | type: string 14 | format: uuid 15 | restaurant_name: 16 | type: string 17 | created_at: 18 | type: string 19 | format: date-time 20 | OrderHistoryFilters: 21 | type: object 22 | properties: 23 | since: 24 | type: string 25 | format: date-time 26 | keywords: 27 | type: array 28 | items: 29 | type: string 30 | status: 31 | type: string 32 | enum: [ ApprovalPending, Approved, Rejected, CancelPending, Cancelled, RevisionPending ] 33 | responses: 34 | GetConsumerOrderHistoryResponse: 35 | description: OK 36 | content: 37 | application/json: 38 | schema: 39 | type: object 40 | required: [ orders, next ] 41 | properties: 42 | orders: 43 | type: array 44 | items: 45 | $ref: '#/components/schemas/OrderHistory' 46 | next: 47 | type: string -------------------------------------------------------------------------------- /order-history/internal/application/service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/order-history/internal/application/commands" 5 | "github.com/stackus/ftgogo/order-history/internal/application/queries" 6 | ) 7 | 8 | type Service struct { 9 | Commands Commands 10 | Queries Queries 11 | } 12 | 13 | type Commands struct { 14 | CreateOrderHistory commands.CreateOrderHistoryHandler 15 | UpdateOrderStatus commands.UpdateOrderStatusHandler 16 | } 17 | 18 | type Queries struct { 19 | SearchOrderHistories queries.SearchOrderHistoriesHandler 20 | GetOrderHistory queries.GetOrderHistoryHandler 21 | } 22 | -------------------------------------------------------------------------------- /order-history/internal/domain/order_history.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/orderapi" 7 | ) 8 | 9 | const OrderHistoryLimit = 20 10 | const OrderHistoryMinimum = 1 11 | const OrderHistoryMaximum = 50 12 | 13 | type OrderHistory struct { 14 | OrderID string 15 | ConsumerID string 16 | RestaurantID string 17 | RestaurantName string 18 | LineItems []orderapi.LineItem 19 | OrderTotal int 20 | Status orderapi.OrderState 21 | Keywords []string 22 | CreatedAt time.Time 23 | } 24 | -------------------------------------------------------------------------------- /order-history/ohismod/setup.go: -------------------------------------------------------------------------------- 1 | package ohismod 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/order-history/internal/adapters" 5 | "github.com/stackus/ftgogo/order-history/internal/application" 6 | "github.com/stackus/ftgogo/order-history/internal/application/commands" 7 | "github.com/stackus/ftgogo/order-history/internal/application/queries" 8 | "github.com/stackus/ftgogo/order-history/internal/handlers" 9 | "shared-go/applications" 10 | ) 11 | 12 | func Setup(svc *applications.Monolith) error { 13 | // Driven 14 | adapters.OrderHistoriesTableName = "orderhistory.orders" 15 | orderHistoryRepo := adapters.NewOrderHistoryPostgresRepository(svc.PgConn) 16 | 17 | app := application.Service{ 18 | Commands: application.Commands{ 19 | CreateOrderHistory: commands.NewCreateOrderHistoryHandler(orderHistoryRepo), 20 | UpdateOrderStatus: commands.NewUpdateOrderStatusHandler(orderHistoryRepo), 21 | }, 22 | Queries: application.Queries{ 23 | SearchOrderHistories: queries.NewSearchOrderHistoriesHandler(orderHistoryRepo), 24 | GetOrderHistory: queries.NewGetOrderHistoryHandler(orderHistoryRepo), 25 | }, 26 | } 27 | 28 | // Drivers 29 | handlers.NewOrderEventHandlers(app).Mount(svc.Subscriber) 30 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /order/cmd/cdc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "shared-go/applications" 5 | ) 6 | 7 | func main() { 8 | cdc := applications.NewCDC(func(*applications.CDC) error { return nil }) 9 | if err := cdc.Execute(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /order/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=order-service -------------------------------------------------------------------------------- /order/feature_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/cucumber/godog" 8 | flag "github.com/spf13/pflag" 9 | 10 | "github.com/stackus/ftgogo/order/features/steps" 11 | ) 12 | 13 | var opts = godog.Options{ 14 | Format: "progress", 15 | NoColors: true, 16 | } 17 | 18 | func init() { 19 | godog.BindCommandLineFlags("godog.", &opts) 20 | } 21 | 22 | func InitializeScenario(ctx *godog.ScenarioContext) { 23 | state := steps.NewFeatureState() 24 | 25 | ctx.BeforeScenario(func(*godog.Scenario) { 26 | state.Reset() 27 | }) 28 | 29 | state.RegisterCommonSteps(ctx) 30 | state.RegisterGetOrderSteps(ctx) 31 | state.RegisterCreateOrderSteps(ctx) 32 | state.RegisterCreateRestaurantSteps(ctx) 33 | state.RegisterCancelOrderSteps(ctx) 34 | state.RegisterReviseOrderSteps(ctx) 35 | } 36 | 37 | func TestFeatures(t *testing.T) { 38 | flag.Parse() 39 | for _, arg := range os.Args[1:] { 40 | if arg == "-test.v=true" { 41 | opts.Format = "pretty" 42 | break 43 | } 44 | } 45 | 46 | status := godog.TestSuite{ 47 | Name: "order features", 48 | ScenarioInitializer: InitializeScenario, 49 | Options: &opts, 50 | }.Run() 51 | 52 | if status != 0 { 53 | t.Fail() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /order/features/approve_order.feature: -------------------------------------------------------------------------------- 1 | @command @order @approve 2 | Feature: Order Approval 3 | 4 | Background: Setup Resources 5 | Given I have initialized the restaurant "Best Foods" 6 | And I have submitted an order to "Best Foods" from "Able Anders" 7 | 8 | Scenario: Pending orders may be approved 9 | Given the order to "Best Foods" from "Able Anders" is "ApprovalPending" 10 | When I approve the order to "Best Foods" from "Able Anders" with ticket "T123" 11 | Then I expect the command to succeed 12 | And the order to "Best Foods" from "Able Anders" is "Approved" 13 | 14 | Scenario: Rejected orders cannot be approved 15 | Given I have rejected the order to "Best Foods" from "Able Anders" 16 | When I approve the order to "Best Foods" from "Able Anders" with ticket "T123" 17 | Then I expect the command to fail 18 | And the returned error message is "order state does not allow action" 19 | -------------------------------------------------------------------------------- /order/features/create_order.feature: -------------------------------------------------------------------------------- 1 | @command @order @create 2 | Feature: Order Creation 3 | 4 | Background: Initialize resources 5 | Given I have initialized the restaurant "Best Foods" 6 | 7 | Scenario: Can create new orders 8 | When I submit an order to "Best Foods" from "Able Anders" 9 | Then I expect the command to succeed 10 | 11 | Scenario: Cannot create new orders at non-existent restaurants 12 | When I submit an order to "Other Foods" from "Able Anders" 13 | Then I expect the command to fail 14 | -------------------------------------------------------------------------------- /order/features/reject_order.feature: -------------------------------------------------------------------------------- 1 | @command @order @reject 2 | Feature: Order Rejection 3 | 4 | Background: Setup Resources 5 | Given I have initialized the restaurant "Best Foods" 6 | And I have submitted an order to "Best Foods" from "Able Anders" 7 | 8 | Scenario: Pending orders may be rejected 9 | Given the order to "Best Foods" from "Able Anders" is "ApprovalPending" 10 | When I reject the order to "Best Foods" from "Able Anders" 11 | Then I expect the command to succeed 12 | And the order to "Best Foods" from "Able Anders" is "Rejected" 13 | 14 | Scenario: Approved orders cannot be rejected 15 | Given I have approved the order to "Best Foods" from "Able Anders" with ticket "T123" 16 | When I reject the order to "Best Foods" from "Able Anders" 17 | Then I expect the command to fail 18 | And the returned error message is "order state does not allow action" 19 | -------------------------------------------------------------------------------- /order/features/steps/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/order/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterCreateRestaurantSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I have (?:created|initialized) the restaurant "([^"]*)"$`, f.iHaveInitializedTheRestaurant) 13 | } 14 | 15 | func (f *FeatureState) iHaveInitializedTheRestaurant(restaurantName string) error { 16 | restaurant, err := getRestaurantFromFixture(restaurantName) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | f.err = f.app.CreateRestaurant(context.Background(), commands.CreateRestaurant{ 22 | RestaurantID: restaurant.RestaurantID, 23 | Name: restaurant.Name, 24 | Menu: restaurant.MenuItems, 25 | }) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /order/features/steps/get_order.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | "github.com/stackus/errors" 8 | 9 | "github.com/stackus/ftgogo/order/internal/application/queries" 10 | ) 11 | 12 | func (f *FeatureState) RegisterGetOrderSteps(ctx *godog.ScenarioContext) { 13 | ctx.Step(`^(?:ensure |expect )?the order to "([^"]*)" from "([^"]*)" is "([^"]*)"$`, f.theOrderToFromIs) 14 | } 15 | 16 | func (f *FeatureState) theOrderToFromIs(restaurantName, consumerName, expected string) error { 17 | orderID := f.orderIDs[restaurantName+consumerName] 18 | 19 | order, err := f.app.GetOrder(context.Background(), queries.GetOrder{OrderID: orderID}) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | got := order.State.String() 25 | if got != expected { 26 | return errors.Wrapf(errors.ErrInvalidArgument, "order state does not match expected: %s: got: %s", expected, got) 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /order/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/order 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/cucumber/godog v0.11.0 16 | github.com/google/uuid v1.3.0 17 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 18 | github.com/hashicorp/go-memdb v1.3.2 // indirect 19 | github.com/mattn/go-colorable v0.1.8 // indirect 20 | github.com/prometheus/client_golang v1.11.0 21 | github.com/rdumont/assistdog v0.0.0-20201106100018-168b06230d14 22 | github.com/spf13/pflag v1.0.5 23 | github.com/stackus/edat v0.0.6 24 | github.com/stackus/edat-msgpack v0.0.2 25 | github.com/stackus/edat-pgx v0.0.2 26 | github.com/stackus/errors v0.0.3 27 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 28 | google.golang.org/grpc v1.39.0 29 | shared-go v0.0.0-00010101000000-000000000000 30 | ) 31 | -------------------------------------------------------------------------------- /order/internal/adapters/inmem_counter.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/order/internal/application/ports" 5 | ) 6 | 7 | // TODO use in integration test of GRPC; Features? 8 | var InmemCounters = map[string]*InmemCounter{} 9 | 10 | type InmemCounter struct { 11 | count float64 12 | } 13 | 14 | var _ ports.Counter = (*InmemCounter)(nil) 15 | 16 | func NewInmemCounter(name string) *InmemCounter { 17 | counter := &InmemCounter{count: 0} 18 | 19 | InmemCounters[name] = counter 20 | 21 | return counter 22 | } 23 | 24 | func (c *InmemCounter) Inc() { 25 | c.count += 1 26 | } 27 | -------------------------------------------------------------------------------- /order/internal/adapters/order_entity_event_publisher.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | ) 8 | 9 | func NewOrderEntityEventPublisher(publisher msg.EntityEventMessagePublisher) ports.OrderPublisher { 10 | return publisher 11 | } 12 | -------------------------------------------------------------------------------- /order/internal/adapters/prometheus_counter.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | func NewPrometheusCounter(name string) prometheus.Counter { 9 | return promauto.NewCounter(prometheus.CounterOpts{Name: name}) 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/application/commands/approve_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type ApproveOrder struct { 11 | OrderID string 12 | TicketID string 13 | } 14 | 15 | type ApproveOrderHandler struct { 16 | repo ports.OrderRepository 17 | counter ports.Counter 18 | } 19 | 20 | func NewApproveOrderHandler(repo ports.OrderRepository, counter ports.Counter) ApproveOrderHandler { 21 | return ApproveOrderHandler{ 22 | repo: repo, 23 | counter: counter, 24 | } 25 | } 26 | 27 | func (h ApproveOrderHandler) Handle(ctx context.Context, cmd ApproveOrder) error { 28 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.ApproveOrder{TicketID: cmd.TicketID}) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | h.counter.Inc() 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /order/internal/application/commands/begin_cancel_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type BeginCancelOrder struct { 11 | OrderID string 12 | } 13 | 14 | type BeginCancelOrderHandler struct { 15 | repo ports.OrderRepository 16 | } 17 | 18 | func NewBeginCancelOrderHandler(orderRepo ports.OrderRepository) BeginCancelOrderHandler { 19 | return BeginCancelOrderHandler{repo: orderRepo} 20 | } 21 | 22 | func (h BeginCancelOrderHandler) Handle(ctx context.Context, cmd BeginCancelOrder) error { 23 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.BeginCancelOrder{}) 24 | 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /order/internal/application/commands/begin_revise_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type BeginReviseOrder struct { 11 | OrderID string 12 | RevisedQuantities map[string]int 13 | } 14 | 15 | type BeginReviseOrderHandler struct { 16 | repo ports.OrderRepository 17 | } 18 | 19 | func NewBeginReviseOrderHandler(repo ports.OrderRepository) BeginReviseOrderHandler { 20 | return BeginReviseOrderHandler{ 21 | repo: repo, 22 | } 23 | } 24 | 25 | func (h BeginReviseOrderHandler) Handle(ctx context.Context, cmd BeginReviseOrder) (int, error) { 26 | order, err := h.repo.Update(ctx, cmd.OrderID, &domain.BeginReviseOrder{ 27 | RevisedQuantities: cmd.RevisedQuantities, 28 | }) 29 | if err != nil { 30 | return 0, err 31 | } 32 | 33 | return order.RevisedOrderTotal(order.OrderTotal(), cmd.RevisedQuantities), nil 34 | } 35 | -------------------------------------------------------------------------------- /order/internal/application/commands/confirm_cancel_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type ConfirmCancelOrder struct { 11 | OrderID string 12 | } 13 | 14 | type ConfirmCancelOrderHandler struct { 15 | repo ports.OrderRepository 16 | } 17 | 18 | func NewConfirmCancelOrderHandler(repo ports.OrderRepository) ConfirmCancelOrderHandler { 19 | return ConfirmCancelOrderHandler{ 20 | repo: repo, 21 | } 22 | } 23 | 24 | func (h ConfirmCancelOrderHandler) Handle(ctx context.Context, cmd ConfirmCancelOrder) error { 25 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.ConfirmCancelOrder{}) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /order/internal/application/commands/confirm_revise_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type ConfirmReviseOrder struct { 11 | OrderID string 12 | RevisedQuantities map[string]int 13 | } 14 | 15 | type ConfirmReviseOrderHandler struct { 16 | repo ports.OrderRepository 17 | } 18 | 19 | func NewConfirmReviseOrderHandler(repo ports.OrderRepository) ConfirmReviseOrderHandler { 20 | return ConfirmReviseOrderHandler{ 21 | repo: repo, 22 | } 23 | } 24 | 25 | func (h ConfirmReviseOrderHandler) Handle(ctx context.Context, cmd ConfirmReviseOrder) error { 26 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.ConfirmReviseOrder{ 27 | RevisedQuantities: cmd.RevisedQuantities, 28 | }) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /order/internal/application/commands/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 9 | ) 10 | 11 | type CreateRestaurant struct { 12 | RestaurantID string 13 | Name string 14 | Menu []restaurantapi.MenuItem 15 | } 16 | 17 | type CreateRestaurantHandler struct { 18 | repo ports.RestaurantRepository 19 | } 20 | 21 | func NewCreateRestaurantHandler(restaurantRepo ports.RestaurantRepository) CreateRestaurantHandler { 22 | return CreateRestaurantHandler{repo: restaurantRepo} 23 | } 24 | 25 | func (h CreateRestaurantHandler) Handle(ctx context.Context, cmd CreateRestaurant) error { 26 | return h.repo.Save(ctx, &domain.Restaurant{ 27 | RestaurantID: cmd.RestaurantID, 28 | Name: cmd.Name, 29 | MenuItems: cmd.Menu, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /order/internal/application/commands/reject_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type RejectOrder struct { 11 | OrderID string 12 | } 13 | 14 | type RejectOrderHandler struct { 15 | repo ports.OrderRepository 16 | counter ports.Counter 17 | } 18 | 19 | func NewRejectOrderHandler(repo ports.OrderRepository, counter ports.Counter) RejectOrderHandler { 20 | return RejectOrderHandler{ 21 | repo: repo, 22 | counter: counter, 23 | } 24 | } 25 | 26 | func (h RejectOrderHandler) Handle(ctx context.Context, cmd RejectOrder) error { 27 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.RejectOrder{}) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | h.counter.Inc() 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /order/internal/application/commands/revise_restaurant_menu.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 8 | ) 9 | 10 | type ReviseRestaurantMenu struct { 11 | RestaurantID string 12 | Menu []restaurantapi.MenuItem 13 | } 14 | 15 | type ReviseRestaurantMenuHandler struct { 16 | repo ports.RestaurantRepository 17 | } 18 | 19 | func NewReviseRestaurantMenuHandler(restaurantRepo ports.RestaurantRepository) ReviseRestaurantMenuHandler { 20 | return ReviseRestaurantMenuHandler{repo: restaurantRepo} 21 | } 22 | 23 | func (h ReviseRestaurantMenuHandler) Handle(ctx context.Context, cmd ReviseRestaurantMenu) error { 24 | restaurant, err := h.repo.Find(ctx, cmd.RestaurantID) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | err = restaurant.ReviseMenu(cmd.Menu) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = h.repo.Update(ctx, cmd.RestaurantID, restaurant) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /order/internal/application/commands/start_cancel_order_saga.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/orderapi" 9 | ) 10 | 11 | type StartCancelOrderSaga struct { 12 | OrderID string 13 | } 14 | 15 | type StartCancelOrderSagaHandler struct { 16 | repo ports.OrderRepository 17 | saga ports.CancelOrderSaga 18 | } 19 | 20 | func NewStartCancelOrderSagaHandler(orderRepo ports.OrderRepository, cancelOrderSaga ports.CancelOrderSaga) StartCancelOrderSagaHandler { 21 | return StartCancelOrderSagaHandler{ 22 | repo: orderRepo, 23 | saga: cancelOrderSaga, 24 | } 25 | } 26 | 27 | func (h StartCancelOrderSagaHandler) Handle(ctx context.Context, cmd StartCancelOrderSaga) (orderapi.OrderState, error) { 28 | order, err := h.repo.Load(ctx, cmd.OrderID) 29 | if err != nil { 30 | return orderapi.UnknownOrderState, err 31 | } 32 | 33 | _, err = h.saga.Start(ctx, &domain.CancelOrderSagaData{ 34 | OrderID: cmd.OrderID, 35 | ConsumerID: order.ConsumerID, 36 | RestaurantID: order.RestaurantID, 37 | TicketID: order.TicketID, 38 | OrderTotal: order.OrderTotal(), 39 | }) 40 | 41 | return orderapi.CancelPending, err 42 | } 43 | -------------------------------------------------------------------------------- /order/internal/application/commands/start_create_order_saga.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/orderapi" 9 | ) 10 | 11 | type StartCreateOrderSaga struct { 12 | OrderID string 13 | ConsumerID string 14 | RestaurantID string 15 | LineItems []orderapi.LineItem 16 | OrderTotal int 17 | } 18 | 19 | type StartCreateOrderSagaHandler struct { 20 | saga ports.CreateOrderSaga 21 | counter ports.Counter 22 | } 23 | 24 | func NewStartCreateOrderSagaHandler(createOrderSaga ports.CreateOrderSaga, ordersPlaced ports.Counter) StartCreateOrderSagaHandler { 25 | return StartCreateOrderSagaHandler{ 26 | saga: createOrderSaga, 27 | counter: ordersPlaced, 28 | } 29 | } 30 | 31 | func (h StartCreateOrderSagaHandler) Handle(ctx context.Context, cmd StartCreateOrderSaga) error { 32 | _, err := h.saga.Start(ctx, &domain.CreateOrderSagaData{ 33 | OrderID: cmd.OrderID, 34 | ConsumerID: cmd.ConsumerID, 35 | RestaurantID: cmd.RestaurantID, 36 | LineItems: cmd.LineItems, 37 | OrderTotal: cmd.OrderTotal, 38 | }) 39 | 40 | h.counter.Inc() 41 | 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /order/internal/application/commands/start_revise_order_saga.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/orderapi" 9 | ) 10 | 11 | type StartReviseOrderSaga struct { 12 | OrderID string 13 | RevisedQuantities map[string]int 14 | } 15 | 16 | type StartReviseOrderSagaHandler struct { 17 | repo ports.OrderRepository 18 | saga ports.ReviseOrderSaga 19 | } 20 | 21 | func NewStartReviseOrderSagaHandler(orderRepo ports.OrderRepository, reviseOrderSaga ports.ReviseOrderSaga) StartReviseOrderSagaHandler { 22 | return StartReviseOrderSagaHandler{ 23 | repo: orderRepo, 24 | saga: reviseOrderSaga, 25 | } 26 | } 27 | 28 | func (h StartReviseOrderSagaHandler) Handle(ctx context.Context, cmd StartReviseOrderSaga) (orderapi.OrderState, error) { 29 | order, err := h.repo.Load(ctx, cmd.OrderID) 30 | if err != nil { 31 | return orderapi.UnknownOrderState, err 32 | } 33 | 34 | _, err = h.saga.Start(ctx, &domain.ReviseOrderSagaData{ 35 | OrderID: cmd.OrderID, 36 | ConsumerID: order.ConsumerID, 37 | RestaurantID: order.RestaurantID, 38 | TicketID: order.TicketID, 39 | RevisedQuantities: cmd.RevisedQuantities, 40 | }) 41 | 42 | return orderapi.RevisionPending, err 43 | } 44 | -------------------------------------------------------------------------------- /order/internal/application/commands/undo_cancel_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type UndoCancelOrder struct { 11 | OrderID string 12 | } 13 | 14 | type UndoCancelOrderHandler struct { 15 | repo ports.OrderRepository 16 | } 17 | 18 | func NewUndoCancelOrderHandler(orderRepo ports.OrderRepository) UndoCancelOrderHandler { 19 | return UndoCancelOrderHandler{repo: orderRepo} 20 | } 21 | 22 | func (h UndoCancelOrderHandler) Handle(ctx context.Context, cmd UndoCancelOrder) error { 23 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.UndoCancelOrder{}) 24 | 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /order/internal/application/commands/undo_revise_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type UndoReviseOrder struct { 11 | OrderID string 12 | } 13 | 14 | type UndoReviseOrderHandler struct { 15 | repo ports.OrderRepository 16 | } 17 | 18 | func NewUndoReviseOrderHandler(orderRepo ports.OrderRepository) UndoReviseOrderHandler { 19 | return UndoReviseOrderHandler{repo: orderRepo} 20 | } 21 | 22 | func (h UndoReviseOrderHandler) Handle(ctx context.Context, cmd UndoReviseOrder) error { 23 | _, err := h.repo.Update(ctx, cmd.OrderID, &domain.UndoReviseOrder{}) 24 | 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /order/internal/application/ports/cancel_order_saga.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/msg" 8 | "github.com/stackus/edat/saga" 9 | ) 10 | 11 | type CancelOrderSaga interface { 12 | Start(ctx context.Context, sagaData core.SagaData) (*saga.Instance, error) 13 | ReplyChannel() string 14 | ReceiveMessage(ctx context.Context, message msg.Message) error 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/application/ports/counter.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | type Counter interface { 4 | Inc() 5 | } 6 | -------------------------------------------------------------------------------- /order/internal/application/ports/create_order_saga.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/msg" 8 | "github.com/stackus/edat/saga" 9 | ) 10 | 11 | type CreateOrderSaga interface { 12 | Start(ctx context.Context, sagaData core.SagaData) (*saga.Instance, error) 13 | ReplyChannel() string 14 | ReceiveMessage(ctx context.Context, message msg.Message) error 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/application/ports/order_publisher.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | ) 6 | 7 | type OrderPublisher interface { 8 | msg.EntityEventMessagePublisher 9 | } 10 | -------------------------------------------------------------------------------- /order/internal/application/ports/order_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/es" 8 | 9 | "github.com/stackus/ftgogo/order/internal/domain" 10 | ) 11 | 12 | type OrderRepository interface { 13 | Load(ctx context.Context, aggregateID string) (*domain.Order, error) 14 | Save(ctx context.Context, command core.Command, options ...es.AggregateRootOption) (*domain.Order, error) 15 | Update(ctx context.Context, aggregateID string, command core.Command, options ...es.AggregateRootOption) (*domain.Order, error) 16 | } 17 | -------------------------------------------------------------------------------- /order/internal/application/ports/restaurant_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/domain" 7 | ) 8 | 9 | type RestaurantRepository interface { 10 | Find(ctx context.Context, restaurantID string) (*domain.Restaurant, error) 11 | Save(ctx context.Context, restaurant *domain.Restaurant) error 12 | Update(ctx context.Context, restaurantID string, restaurant *domain.Restaurant) error 13 | } 14 | -------------------------------------------------------------------------------- /order/internal/application/ports/revise_order_saga.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/core" 7 | "github.com/stackus/edat/msg" 8 | "github.com/stackus/edat/saga" 9 | ) 10 | 11 | type ReviseOrderSaga interface { 12 | Start(ctx context.Context, sagaData core.SagaData) (*saga.Instance, error) 13 | ReplyChannel() string 14 | ReceiveMessage(ctx context.Context, message msg.Message) error 15 | } 16 | -------------------------------------------------------------------------------- /order/internal/application/queries/get_order.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type GetOrder struct { 11 | OrderID string 12 | } 13 | 14 | type GetOrderHandler struct { 15 | orderRepo ports.OrderRepository 16 | } 17 | 18 | func NewGetOrderHandler(orderRepo ports.OrderRepository) GetOrderHandler { 19 | return GetOrderHandler{orderRepo: orderRepo} 20 | } 21 | 22 | func (h GetOrderHandler) Handle(ctx context.Context, query GetOrder) (*domain.Order, error) { 23 | return h.orderRepo.Load(ctx, query.OrderID) 24 | } 25 | -------------------------------------------------------------------------------- /order/internal/application/queries/get_restaurant.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/order/internal/application/ports" 7 | "github.com/stackus/ftgogo/order/internal/domain" 8 | ) 9 | 10 | type GetRestaurant struct { 11 | RestaurantID string 12 | } 13 | 14 | type GetRestaurantHandler struct { 15 | repo ports.RestaurantRepository 16 | } 17 | 18 | func NewGetRestaurantHandler(restaurantRepo ports.RestaurantRepository) GetRestaurantHandler { 19 | return GetRestaurantHandler{repo: restaurantRepo} 20 | } 21 | 22 | func (h GetRestaurantHandler) Handle(ctx context.Context, query GetRestaurant) (*domain.Restaurant, error) { 23 | restaurant, err := h.repo.Find(ctx, query.RestaurantID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return restaurant, nil 29 | } 30 | -------------------------------------------------------------------------------- /order/internal/domain/cancel_order_saga_data.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerCancelOrderSagaData() { 8 | core.RegisterSagaData(CancelOrderSagaData{}) 9 | } 10 | 11 | type CancelOrderSagaData struct { 12 | OrderID string 13 | ConsumerID string 14 | RestaurantID string 15 | TicketID string 16 | OrderTotal int 17 | } 18 | 19 | func (CancelOrderSagaData) SagaDataName() string { 20 | return "orderservice.CancelOrderSagaData" 21 | } 22 | -------------------------------------------------------------------------------- /order/internal/domain/create_order_saga_data.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/orderapi" 7 | ) 8 | 9 | func registerCreateOrderSagaData() { 10 | core.RegisterSagaData(CreateOrderSagaData{}) 11 | } 12 | 13 | type CreateOrderSagaData struct { 14 | OrderID string 15 | TicketID string 16 | ConsumerID string 17 | RestaurantID string 18 | LineItems []orderapi.LineItem 19 | OrderTotal int 20 | } 21 | 22 | func (CreateOrderSagaData) SagaDataName() string { 23 | return "orderservice.CreateOrderSagaData" 24 | } 25 | -------------------------------------------------------------------------------- /order/internal/domain/order_events.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerOrderEvents() { 8 | core.RegisterEvents( 9 | OrderCancelling{}, OrderCancellingUndone{}, 10 | OrderRevisingUndone{}, 11 | ) 12 | } 13 | 14 | type OrderCancelling struct{} 15 | 16 | // EventName event method 17 | func (OrderCancelling) EventName() string { return "orderservice.OrderCancelling" } 18 | 19 | type OrderCancellingUndone struct{} 20 | 21 | // EventName event method 22 | func (OrderCancellingUndone) EventName() string { return "orderservice.OrderCancellingUndone" } 23 | 24 | type OrderRevisingUndone struct{} 25 | 26 | // EventName event method 27 | func (OrderRevisingUndone) EventName() string { return "orderservice.OrderRevisingUndone" } 28 | -------------------------------------------------------------------------------- /order/internal/domain/order_snapshots.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/edat/core" 7 | 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | "github.com/stackus/ftgogo/serviceapis/orderapi" 10 | ) 11 | 12 | func registerOrderSnapshots() { 13 | core.RegisterSnapshots(OrderSnapshot{}) 14 | } 15 | 16 | type OrderSnapshot struct { 17 | ConsumerID string 18 | RestaurantID string 19 | TicketID string 20 | LineItems []orderapi.LineItem 21 | DeliverAt time.Time 22 | DeliverTo *commonapi.Address 23 | State orderapi.OrderState 24 | } 25 | 26 | func (OrderSnapshot) SnapshotName() string { return "orderservice.OrderSnapshot" } 27 | -------------------------------------------------------------------------------- /order/internal/domain/register_types.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | func RegisterTypes() { 4 | registerOrderCommands() 5 | registerOrderEvents() 6 | registerOrderSnapshots() 7 | registerCancelOrderSagaData() 8 | registerCreateOrderSagaData() 9 | registerReviseOrderSagaData() 10 | } 11 | -------------------------------------------------------------------------------- /order/internal/domain/restaurant.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 5 | 6 | "github.com/stackus/errors" 7 | ) 8 | 9 | // Restaurant errors 10 | var ( 11 | ErrRestaurantNotFound = errors.Wrap(errors.ErrNotFound, "restaurant not found") 12 | ErrMenuItemNotFound = errors.Wrap(errors.ErrNotFound, "menu item not found") 13 | ) 14 | 15 | type Restaurant struct { 16 | RestaurantID string 17 | Name string 18 | // note: no address 19 | MenuItems []restaurantapi.MenuItem 20 | } 21 | 22 | // FindMenuItem locates the local menu item record for a given menuItemID 23 | func (r *Restaurant) FindMenuItem(menuItemID string) (restaurantapi.MenuItem, error) { 24 | for _, item := range r.MenuItems { 25 | if menuItemID == item.ID { 26 | return item, nil 27 | } 28 | } 29 | 30 | return restaurantapi.MenuItem{}, errors.Wrap(ErrMenuItemNotFound, menuItemID) 31 | } 32 | 33 | // ReviseMenu updates the local menu 34 | // NOT IMPLEMENTED 35 | func (r *Restaurant) ReviseMenu(_ []restaurantapi.MenuItem) error { 36 | return errors.ErrUnimplemented 37 | } 38 | -------------------------------------------------------------------------------- /order/internal/domain/revise_order_saga_data.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerReviseOrderSagaData() { 8 | core.RegisterSagaData(ReviseOrderSagaData{}) 9 | } 10 | 11 | type ReviseOrderSagaData struct { 12 | OrderID string 13 | ConsumerID string 14 | RestaurantID string 15 | TicketID string 16 | ExpectedVersion int 17 | RevisedOrderTotal int 18 | RevisedQuantities map[string]int 19 | } 20 | 21 | func (ReviseOrderSagaData) SagaDataName() string { 22 | return "orderservice.ReviseOrderSagaData" 23 | } 24 | -------------------------------------------------------------------------------- /order/internal/handlers/order_event_handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/edat/msg" 7 | 8 | "github.com/stackus/ftgogo/order/internal/application" 9 | "github.com/stackus/ftgogo/order/internal/application/commands" 10 | "github.com/stackus/ftgogo/serviceapis/orderapi" 11 | ) 12 | 13 | type OrderEventHandlers struct { 14 | app application.ServiceApplication 15 | } 16 | 17 | func NewOrderEventHandlers(app application.ServiceApplication) OrderEventHandlers { 18 | return OrderEventHandlers{app: app} 19 | } 20 | 21 | func (h OrderEventHandlers) Mount(subscriber *msg.Subscriber) { 22 | subscriber.Subscribe(orderapi.OrderAggregateChannel, msg.NewEntityEventDispatcher(). 23 | Handle(orderapi.OrderCreated{}, h.OrderCreated)) 24 | } 25 | 26 | func (h OrderEventHandlers) OrderCreated(ctx context.Context, evtMsg msg.EntityEvent) error { 27 | evt := evtMsg.Event().(*orderapi.OrderCreated) 28 | 29 | err := h.app.StartCreateOrderSaga(ctx, commands.StartCreateOrderSaga{ 30 | OrderID: evtMsg.EntityID(), 31 | ConsumerID: evt.ConsumerID, 32 | RestaurantID: evt.RestaurantID, 33 | LineItems: evt.LineItems, 34 | OrderTotal: evt.OrderTotal, 35 | }) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /restaurant/cmd/cdc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "shared-go/applications" 5 | ) 6 | 7 | func main() { 8 | cdc := applications.NewCDC(func(*applications.CDC) error { return nil }) 9 | if err := cdc.Execute(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /restaurant/cmd/service/.env: -------------------------------------------------------------------------------- 1 | SERVICE_ID=restaurant-service -------------------------------------------------------------------------------- /restaurant/cmd/service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/restaurant/internal/adapters" 5 | "github.com/stackus/ftgogo/restaurant/internal/application" 6 | "github.com/stackus/ftgogo/restaurant/internal/handlers" 7 | "github.com/stackus/ftgogo/serviceapis" 8 | "shared-go/applications" 9 | ) 10 | 11 | func main() { 12 | svc := applications.NewService(initService) 13 | if err := svc.Execute(); err != nil { 14 | panic(err) 15 | } 16 | } 17 | 18 | func initService(svc *applications.Service) error { 19 | serviceapis.RegisterTypes() 20 | 21 | // Driven 22 | restaurantRepo := adapters.NewRestaurantPostgresPublisherMiddleware( 23 | adapters.NewRestaurantPostgresRepository(svc.PgConn), 24 | adapters.NewRestaurantEntityEventPublisher(svc.Publisher), 25 | ) 26 | 27 | app := application.NewServiceApplication(restaurantRepo) 28 | 29 | // Drivers 30 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /restaurant/feature_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/cucumber/godog" 8 | flag "github.com/spf13/pflag" 9 | 10 | "github.com/stackus/ftgogo/restaurant/features/steps" 11 | ) 12 | 13 | var opts = godog.Options{ 14 | Format: "progress", 15 | NoColors: true, 16 | } 17 | 18 | func init() { 19 | godog.BindCommandLineFlags("godog.", &opts) 20 | } 21 | 22 | func InitializeScenario(ctx *godog.ScenarioContext) { 23 | state := steps.NewFeatureState() 24 | 25 | ctx.BeforeScenario(func(*godog.Scenario) { 26 | state.Reset() 27 | }) 28 | 29 | state.RegisterCommonSteps(ctx) 30 | state.RegisterCreateRestaurantSteps(ctx) 31 | } 32 | 33 | func TestFeatures(t *testing.T) { 34 | flag.Parse() 35 | for _, arg := range os.Args[1:] { 36 | if arg == "-test.v=true" { 37 | opts.Format = "pretty" 38 | break 39 | } 40 | } 41 | 42 | status := godog.TestSuite{ 43 | Name: "restaurant features", 44 | ScenarioInitializer: InitializeScenario, 45 | Options: &opts, 46 | }.Run() 47 | 48 | if status != 0 { 49 | t.Fail() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /restaurant/features/create_restaurant.feature: -------------------------------------------------------------------------------- 1 | Feature: Restaurant Creation 2 | 3 | Scenario: Can create new restaurants 4 | When I create the restaurant "Best Foods" 5 | Then I expect the command to succeed 6 | -------------------------------------------------------------------------------- /restaurant/features/steps/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package steps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cucumber/godog" 7 | 8 | "github.com/stackus/ftgogo/restaurant/internal/application/commands" 9 | ) 10 | 11 | func (f *FeatureState) RegisterCreateRestaurantSteps(ctx *godog.ScenarioContext) { 12 | ctx.Step(`^I (?:have )?(?:create|initialize)d? the restaurant "([^"]*)"$`, f.iCreateTheRestaurant) 13 | } 14 | 15 | func (f *FeatureState) iCreateTheRestaurant(restaurantName string) error { 16 | restaurant, err := getRestaurantFromFixture(restaurantName) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | f.restaurantIDs[restaurantName], f.err = f.app.CreateRestaurant(context.Background(), commands.CreateRestaurant{ 22 | Name: restaurant.Name, 23 | Address: restaurant.Address, 24 | MenuItems: restaurant.MenuItems, 25 | }) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /restaurant/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/restaurant 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/cucumber/godog v0.11.0 16 | github.com/google/uuid v1.3.0 17 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 18 | github.com/hashicorp/go-memdb v1.3.2 // indirect 19 | github.com/mattn/go-colorable v0.1.8 // indirect 20 | github.com/rdumont/assistdog v0.0.0-20201106100018-168b06230d14 21 | github.com/spf13/pflag v1.0.5 22 | github.com/stackus/edat v0.0.6 23 | github.com/stackus/edat-msgpack v0.0.2 24 | github.com/stackus/edat-pgx v0.0.2 25 | github.com/stackus/errors v0.0.3 26 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 27 | google.golang.org/grpc v1.39.0 28 | shared-go v0.0.0-00010101000000-000000000000 29 | ) 30 | -------------------------------------------------------------------------------- /restaurant/internal/adapters/restaurant_entity_event_publisher.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | 6 | "github.com/stackus/ftgogo/restaurant/internal/application/ports" 7 | ) 8 | 9 | func NewRestaurantEntityEventPublisher(publisher msg.EntityEventMessagePublisher) ports.RestaurantPublisher { 10 | return publisher 11 | } 12 | -------------------------------------------------------------------------------- /restaurant/internal/application/commands/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/restaurant/internal/application/ports" 7 | "github.com/stackus/ftgogo/restaurant/internal/domain" 8 | "github.com/stackus/ftgogo/serviceapis/commonapi" 9 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 10 | ) 11 | 12 | type CreateRestaurant struct { 13 | Name string 14 | Address *commonapi.Address 15 | MenuItems []restaurantapi.MenuItem 16 | } 17 | 18 | type CreateRestaurantHandler struct { 19 | repo ports.RestaurantRepository 20 | } 21 | 22 | func NewCreateRestaurantHandler(restaurantRepo ports.RestaurantRepository) CreateRestaurantHandler { 23 | return CreateRestaurantHandler{ 24 | repo: restaurantRepo, 25 | } 26 | } 27 | 28 | func (h CreateRestaurantHandler) Handle(ctx context.Context, cmd CreateRestaurant) (string, error) { 29 | restaurant := domain.CreateRestaurant(cmd.Name, cmd.Address, cmd.MenuItems) 30 | 31 | err := h.repo.Save(ctx, restaurant) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | return restaurant.RestaurantID, nil 37 | } 38 | -------------------------------------------------------------------------------- /restaurant/internal/application/ports/restaurant_publisher.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/stackus/edat/msg" 5 | ) 6 | 7 | type RestaurantPublisher interface { 8 | msg.EntityEventMessagePublisher 9 | } 10 | -------------------------------------------------------------------------------- /restaurant/internal/application/ports/restaurant_repository.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/restaurant/internal/domain" 7 | ) 8 | 9 | type RestaurantRepository interface { 10 | Find(ctx context.Context, restaurantID string) (*domain.Restaurant, error) 11 | Save(ctx context.Context, restaurant *domain.Restaurant) error 12 | Update(ctx context.Context, restaurantID string, restaurant *domain.Restaurant) error 13 | } 14 | -------------------------------------------------------------------------------- /restaurant/internal/application/queries/get_restaurant.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/restaurant/internal/application/ports" 7 | "github.com/stackus/ftgogo/restaurant/internal/domain" 8 | ) 9 | 10 | type GetRestaurant struct { 11 | RestaurantID string 12 | } 13 | 14 | type GetRestaurantHandler struct { 15 | repo ports.RestaurantRepository 16 | } 17 | 18 | func NewGetRestaurantHandler(repo ports.RestaurantRepository) GetRestaurantHandler { 19 | return GetRestaurantHandler{repo: repo} 20 | } 21 | 22 | func (h GetRestaurantHandler) Handle(ctx context.Context, query GetRestaurant) (*domain.Restaurant, error) { 23 | restaurant, err := h.repo.Find(ctx, query.RestaurantID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return restaurant, nil 29 | } 30 | -------------------------------------------------------------------------------- /restaurant/restmod/setup.go: -------------------------------------------------------------------------------- 1 | package restmod 2 | 3 | import ( 4 | edatpgx "github.com/stackus/edat-pgx" 5 | "github.com/stackus/edat/msg" 6 | "github.com/stackus/edat/outbox" 7 | 8 | "github.com/stackus/ftgogo/restaurant/internal/adapters" 9 | "github.com/stackus/ftgogo/restaurant/internal/application" 10 | "github.com/stackus/ftgogo/restaurant/internal/handlers" 11 | "github.com/stackus/ftgogo/serviceapis" 12 | "shared-go/applications" 13 | ) 14 | 15 | func Setup(svc *applications.Monolith) error { 16 | serviceapis.RegisterTypes() 17 | 18 | // Infrastructure 19 | messageStore := edatpgx.NewMessageStore(svc.CDCPgConn, edatpgx.WithMessageStoreTableName("restaurant.messages")) 20 | publisher := msg.NewPublisher(messageStore) 21 | svc.Publishers = append(svc.Publishers, publisher) 22 | svc.Processors = append(svc.Processors, outbox.NewPollingProcessor(messageStore, svc.CDCPublisher)) 23 | 24 | // Driven 25 | adapters.RestaurantsTableName = "restaurant.restaurants" 26 | restaurantRepo := adapters.NewRestaurantPostgresPublisherMiddleware( 27 | adapters.NewRestaurantPostgresRepository(svc.PgConn), 28 | adapters.NewRestaurantEntityEventPublisher(publisher), 29 | ) 30 | 31 | app := application.NewServiceApplication(restaurantRepo) 32 | 33 | // Drivers 34 | handlers.NewRpcHandlers(app).Mount(svc.RpcServer) 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /serviceapis/Makefile: -------------------------------------------------------------------------------- 1 | compile: 2 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./commonapi/pb/service.proto 3 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./accountingapi/pb/service.proto 4 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./consumerapi/pb/service.proto 5 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./deliveryapi/pb/service.proto 6 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./kitchenapi/pb/service.proto 7 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./orderapi/pb/service.proto 8 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./orderhistoryapi/pb/service.proto 9 | protoc -I=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./restaurantapi/pb/service.proto 10 | -------------------------------------------------------------------------------- /serviceapis/accountingapi/api.go: -------------------------------------------------------------------------------- 1 | package accountingapi 2 | 3 | // Service Commands 4 | const AccountingServiceCommandChannel = "accountingservice" 5 | 6 | func RegisterTypes() { 7 | registerCommands() 8 | registerReplies() 9 | } 10 | -------------------------------------------------------------------------------- /serviceapis/accountingapi/commands.go: -------------------------------------------------------------------------------- 1 | package accountingapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerCommands() { 8 | core.RegisterCommands(AuthorizeOrder{}, ReverseAuthorizeOrder{}, ReviseAuthorizeOrder{}) 9 | } 10 | 11 | type AccountServiceCommand struct{} 12 | 13 | func (AccountServiceCommand) DestinationChannel() string { return AccountingServiceCommandChannel } 14 | 15 | type AuthorizeOrder struct { 16 | AccountServiceCommand 17 | ConsumerID string 18 | OrderID string 19 | OrderTotal int // Money 20 | } 21 | 22 | func (AuthorizeOrder) CommandName() string { return "accountingapi.AuthorizeOrder" } 23 | 24 | type ReverseAuthorizeOrder struct { 25 | AccountServiceCommand 26 | ConsumerID string 27 | OrderID string 28 | OrderTotal int // Money 29 | } 30 | 31 | func (ReverseAuthorizeOrder) CommandName() string { return "accountingapi.ReverseAuthorizeOrder" } 32 | 33 | type ReviseAuthorizeOrder struct { 34 | AccountServiceCommand 35 | ConsumerID string 36 | OrderID string 37 | OrderTotal int // Money 38 | } 39 | 40 | func (ReviseAuthorizeOrder) CommandName() string { return "accountingapi.ReviseAuthorizeOrder" } 41 | -------------------------------------------------------------------------------- /serviceapis/accountingapi/pb/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/stackus/ftgogo/serviceapis/accountingapi/pb;accountingpb"; 4 | 5 | package accountingpb; 6 | 7 | import 'google/protobuf/empty.proto'; 8 | 9 | service AccountingService { 10 | rpc GetAccount(GetAccountRequest) returns (GetAccountResponse); 11 | rpc DisableAccount(DisableAccountRequest) returns (google.protobuf.Empty); 12 | rpc EnableAccount(EnableAccountRequest) returns (google.protobuf.Empty); 13 | } 14 | 15 | message GetAccountRequest { 16 | string AccountID = 1; 17 | } 18 | 19 | message GetAccountResponse { 20 | string AccountID = 1; 21 | bool Enabled = 2; 22 | } 23 | 24 | message DisableAccountRequest { 25 | string AccountID = 1; 26 | } 27 | 28 | message EnableAccountRequest { 29 | string AccountID = 1; 30 | } 31 | -------------------------------------------------------------------------------- /serviceapis/accountingapi/replies.go: -------------------------------------------------------------------------------- 1 | package accountingapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerReplies() { 8 | core.RegisterReplies(AccountDisabled{}) 9 | } 10 | 11 | type AccountDisabled struct{} 12 | 13 | func (AccountDisabled) ReplyName() string { return "accountingapi.AccountDisabled" } 14 | -------------------------------------------------------------------------------- /serviceapis/commonapi/pb/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/stackus/ftgogo/serviceapis/commonapi/pb;commonpb"; 4 | 5 | package commonpb; 6 | 7 | message Address { 8 | string Street1 = 1; 9 | string Street2 = 2; 10 | string City = 3; 11 | string State = 4; 12 | string Zip = 5; 13 | } 14 | 15 | message MenuItemQuantities { 16 | map Items = 1; 17 | } 18 | -------------------------------------------------------------------------------- /serviceapis/commonapi/spec.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Address: 4 | type: object 5 | required: [ street1, city, state, zip ] 6 | properties: 7 | street1: 8 | type: string 9 | street2: 10 | type: string 11 | city: 12 | type: string 13 | state: 14 | type: string 15 | zip: 16 | type: string 17 | MenuItemQuantities: 18 | type: object 19 | additionalProperties: 20 | type: integer 21 | -------------------------------------------------------------------------------- /serviceapis/consumerapi/api.go: -------------------------------------------------------------------------------- 1 | package consumerapi 2 | 3 | // Service Commands 4 | const ConsumerServiceCommandChannel = "consumerservice" 5 | 6 | // Aggregates 7 | const ConsumerAggregateChannel = "consumerservice.Consumer" 8 | 9 | func RegisterTypes() { 10 | registerCommands() 11 | registerEvents() 12 | registerReplies() 13 | } 14 | -------------------------------------------------------------------------------- /serviceapis/consumerapi/commands.go: -------------------------------------------------------------------------------- 1 | package consumerapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerCommands() { 8 | core.RegisterCommands(ValidateOrderByConsumer{}) 9 | } 10 | 11 | type ConsumerServiceCommand struct{} 12 | 13 | func (ConsumerServiceCommand) DestinationChannel() string { return ConsumerServiceCommandChannel } 14 | 15 | type ValidateOrderByConsumer struct { 16 | ConsumerServiceCommand 17 | ConsumerID string 18 | OrderID string 19 | OrderTotal int // Money 20 | } 21 | 22 | func (ValidateOrderByConsumer) CommandName() string { return "consumerapi.ValidateOrderByConsumer" } 23 | -------------------------------------------------------------------------------- /serviceapis/consumerapi/replies.go: -------------------------------------------------------------------------------- 1 | package consumerapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerReplies() { 8 | core.RegisterReplies(ConsumerNotFound{}) 9 | } 10 | 11 | type ConsumerNotFound struct{} 12 | 13 | func (ConsumerNotFound) ReplyName() string { return "consumerapi.ConsumerNotFound" } 14 | -------------------------------------------------------------------------------- /serviceapis/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/serviceapis 2 | 3 | go 1.16 4 | 5 | // Development replacements 6 | //replace github.com/stackus/edat => ../../edat 7 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 8 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 9 | 10 | require ( 11 | github.com/getkin/kin-openapi v0.68.0 12 | github.com/go-openapi/swag v0.19.15 // indirect 13 | github.com/mailru/easyjson v0.7.7 // indirect 14 | github.com/stackus/edat v0.0.6 15 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect 16 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 17 | google.golang.org/genproto v0.0.0-20210729151513-df9385d47c1b // indirect 18 | google.golang.org/grpc v1.39.0 19 | google.golang.org/protobuf v1.27.1 20 | ) 21 | -------------------------------------------------------------------------------- /serviceapis/kitchenapi/api.go: -------------------------------------------------------------------------------- 1 | package kitchenapi 2 | 3 | // Service Commands 4 | const KitchenServiceCommandChannel = "kitchenservice" 5 | 6 | // Aggregates 7 | const TicketAggregateChannel = "kitchenservice.Ticket" 8 | 9 | func RegisterTypes() { 10 | registerCommands() 11 | registerEvents() 12 | registerReplies() 13 | } 14 | -------------------------------------------------------------------------------- /serviceapis/kitchenapi/entities.go: -------------------------------------------------------------------------------- 1 | package kitchenapi 2 | 3 | type LineItem struct { 4 | MenuItemID string 5 | Name string 6 | Quantity int 7 | } 8 | -------------------------------------------------------------------------------- /serviceapis/kitchenapi/events.go: -------------------------------------------------------------------------------- 1 | package kitchenapi 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/edat/core" 7 | ) 8 | 9 | func registerEvents() { 10 | core.RegisterEvents( 11 | TicketCreated{}, TicketCancelled{}, TicketRevised{}, 12 | TicketAccepted{}, 13 | ) 14 | } 15 | 16 | type TicketEvent struct{} 17 | 18 | func (TicketEvent) DestinationChannel() string { return TicketAggregateChannel } 19 | 20 | type TicketCreated struct { 21 | TicketEvent 22 | OrderID string 23 | RestaurantID string 24 | LineItems []LineItem 25 | } 26 | 27 | func (TicketCreated) EventName() string { return "kitchenapi.TicketCreated" } 28 | 29 | type TicketCancelled struct { 30 | TicketEvent 31 | OrderID string 32 | } 33 | 34 | func (TicketCancelled) EventName() string { return "kitchenapi.TicketCancelled" } 35 | 36 | type TicketRevised struct { 37 | TicketEvent 38 | RevisedQuantities map[string]int 39 | } 40 | 41 | func (TicketRevised) EventName() string { return "kitchenapi.TicketRevised" } 42 | 43 | type TicketAccepted struct { 44 | TicketEvent 45 | OrderID string 46 | AcceptedAt time.Time 47 | ReadyBy time.Time 48 | } 49 | 50 | func (TicketAccepted) EventName() string { return "kitchenapi.TicketAccepted" } 51 | -------------------------------------------------------------------------------- /serviceapis/kitchenapi/pb/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/stackus/ftgogo/serviceapis/kitchenapi/pb;kitchenpb"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | package kitchenpb; 8 | 9 | service KitchenService { 10 | rpc GetRestaurant(GetRestaurantRequest) returns (GetRestaurantResponse); 11 | rpc AcceptTicket(AcceptTicketRequest) returns (AcceptTicketResponse); 12 | } 13 | 14 | message GetRestaurantRequest { 15 | string RestaurantID = 1; 16 | } 17 | 18 | message GetRestaurantResponse { 19 | string RestaurantID = 1; 20 | } 21 | 22 | message AcceptTicketRequest { 23 | string TicketID = 1; 24 | google.protobuf.Timestamp ReadyBy = 2; 25 | } 26 | 27 | message AcceptTicketResponse { 28 | string TicketID = 1; 29 | } 30 | -------------------------------------------------------------------------------- /serviceapis/kitchenapi/replies.go: -------------------------------------------------------------------------------- 1 | package kitchenapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerReplies() { 8 | core.RegisterReplies(CreateTicketReply{}) 9 | } 10 | 11 | type CreateTicketReply struct { 12 | TicketID string 13 | } 14 | 15 | func (CreateTicketReply) ReplyName() string { return "kitchenapi.CreateTicketReply" } 16 | -------------------------------------------------------------------------------- /serviceapis/orderapi/api.go: -------------------------------------------------------------------------------- 1 | package orderapi 2 | 3 | // Service Commands 4 | const OrderServiceCommandChannel = "orderservice" 5 | 6 | // Aggregates 7 | const OrderAggregateChannel = "orderservice.Order" 8 | 9 | // Sagas 10 | const CreateOrderSagaChannel = "orderservice.CreateOrderSaga" 11 | const CancelOrderSagaChannel = "orderservice.CancelOrderSaga" 12 | const ReviseOrderSagaChannel = "orderservice.ReviseOrderSaga" 13 | 14 | func RegisterTypes() { 15 | registerCommands() 16 | registerEvents() 17 | registerReplies() 18 | } 19 | -------------------------------------------------------------------------------- /serviceapis/orderapi/replies.go: -------------------------------------------------------------------------------- 1 | package orderapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerReplies() { 8 | core.RegisterReplies(BeginReviseOrderReply{}) 9 | } 10 | 11 | type BeginReviseOrderReply struct { 12 | RevisedOrderTotal int 13 | } 14 | 15 | func (BeginReviseOrderReply) ReplyName() string { return "orderapi.BeginReviseOrderReply" } 16 | -------------------------------------------------------------------------------- /serviceapis/orderhistoryapi/pb/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/stackus/ftgogo/serviceapis/orderhistoryapi/pb;orderhistorypb"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | import "orderapi/pb/service.proto"; 7 | 8 | package orderhistorypb; 9 | 10 | service OrderHistoryService { 11 | rpc SearchOrderHistories(SearchOrderHistoriesRequest) returns (SearchOrderHistoriesResponse); 12 | rpc GetOrderHistory(GetOrderHistoryRequest) returns (GetOrderHistoryResponse); 13 | } 14 | 15 | message OrderHistory { 16 | string OrderID = 1; 17 | string ConsumerID = 2; 18 | string RestaurantID = 3; 19 | string RestaurantName = 4; 20 | orderpb.OrderState Status = 5; 21 | google.protobuf.Timestamp CreatedAt = 6; 22 | } 23 | 24 | message SearchOrderHistoriesRequest { 25 | message filters { 26 | google.protobuf.Timestamp Since = 1; 27 | repeated string Keywords = 2; 28 | orderpb.OrderState Status = 3; 29 | } 30 | 31 | string ConsumerID = 1; 32 | filters Filter = 2; 33 | string Next = 3; 34 | int64 Limit = 4; 35 | } 36 | 37 | message SearchOrderHistoriesResponse { 38 | repeated OrderHistory Orders = 1; 39 | string Next = 2; 40 | } 41 | 42 | message GetOrderHistoryRequest { 43 | string OrderID = 1; 44 | } 45 | 46 | message GetOrderHistoryResponse { 47 | OrderHistory Order = 1; 48 | } 49 | -------------------------------------------------------------------------------- /serviceapis/register_types.go: -------------------------------------------------------------------------------- 1 | package serviceapis 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/serviceapis/accountingapi" 5 | "github.com/stackus/ftgogo/serviceapis/consumerapi" 6 | "github.com/stackus/ftgogo/serviceapis/kitchenapi" 7 | "github.com/stackus/ftgogo/serviceapis/orderapi" 8 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 9 | ) 10 | 11 | func RegisterTypes() { 12 | accountingapi.RegisterTypes() 13 | consumerapi.RegisterTypes() 14 | kitchenapi.RegisterTypes() 15 | orderapi.RegisterTypes() 16 | restaurantapi.RegisterTypes() 17 | } 18 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/api.go: -------------------------------------------------------------------------------- 1 | package restaurantapi 2 | 3 | // Service Commands 4 | const RestaurantServiceCommandChannel = "restaurantservice" 5 | 6 | // Aggregates 7 | const RestaurantAggregateChannel = "restaurantservice.Restaurant" 8 | 9 | func RegisterTypes() { 10 | registerCommands() 11 | registerEvents() 12 | registerReplies() 13 | } 14 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/commands.go: -------------------------------------------------------------------------------- 1 | package restaurantapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerCommands() { 8 | core.RegisterCommands(ValidateMenuItems{}) 9 | } 10 | 11 | type RestaurantServiceCommand struct{} 12 | 13 | func (RestaurantServiceCommand) DestinationChannel() string { return RestaurantServiceCommandChannel } 14 | 15 | type ValidateMenuItems struct { 16 | RestaurantServiceCommand 17 | RestaurantID string 18 | MenuItems []string 19 | } 20 | 21 | func (ValidateMenuItems) CommandName() string { return "restaurantapi.ValidateMenuItems" } 22 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/entities.go: -------------------------------------------------------------------------------- 1 | package restaurantapi 2 | 3 | import ( 4 | restaurantpb "github.com/stackus/ftgogo/serviceapis/restaurantapi/pb" 5 | ) 6 | 7 | type MenuItem struct { 8 | ID string 9 | Name string 10 | Price int 11 | } 12 | 13 | func ToMenuItemProto(menuItem MenuItem) *restaurantpb.MenuItem { 14 | return &restaurantpb.MenuItem{ 15 | ID: menuItem.ID, 16 | Name: menuItem.Name, 17 | Price: int64(menuItem.Price), 18 | } 19 | } 20 | 21 | func FromMenuItemProto(menuItem *restaurantpb.MenuItem) MenuItem { 22 | return MenuItem{ 23 | ID: menuItem.ID, 24 | Name: menuItem.Name, 25 | Price: int(menuItem.Price), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/events.go: -------------------------------------------------------------------------------- 1 | package restaurantapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | ) 8 | 9 | func registerEvents() { 10 | core.RegisterEvents(RestaurantCreated{}, RestaurantMenuRevised{}) 11 | } 12 | 13 | type RestaurantEvent struct{} 14 | 15 | func (RestaurantEvent) DestinationChannel() string { return RestaurantAggregateChannel } 16 | 17 | type RestaurantCreated struct { 18 | RestaurantEvent 19 | Name string 20 | Address *commonapi.Address 21 | Menu []MenuItem 22 | } 23 | 24 | func (RestaurantCreated) EventName() string { return "restaurantapi.RestaurantCreated" } 25 | 26 | type RestaurantMenuRevised struct { 27 | RestaurantEvent 28 | Menu []MenuItem 29 | } 30 | 31 | func (RestaurantMenuRevised) EventName() string { return "restaurantapi.RestaurantMenuRevised" } 32 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/pb/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/stackus/ftgogo/serviceapis/restaurantapi/pb;restaurantpb"; 4 | 5 | package restaurantpb; 6 | 7 | import 'commonapi/pb/service.proto'; 8 | 9 | service RestaurantService { 10 | rpc CreateRestaurant(CreateRestaurantRequest) returns (CreateRestaurantResponse); 11 | rpc GetRestaurant(GetRestaurantRequest) returns (GetRestaurantResponse); 12 | } 13 | 14 | message MenuItem { 15 | string ID = 1; 16 | string Name = 2; 17 | int64 Price = 3; 18 | } 19 | 20 | message Menu { 21 | repeated MenuItem MenuItems = 1; 22 | } 23 | 24 | message CreateRestaurantRequest { 25 | string Name = 1; 26 | commonpb.Address Address = 2; 27 | Menu Menu = 3; 28 | } 29 | 30 | message CreateRestaurantResponse { 31 | string RestaurantID = 1; 32 | } 33 | 34 | message GetRestaurantRequest { 35 | string RestaurantID = 1; 36 | } 37 | 38 | message GetRestaurantResponse { 39 | string RestaurantID = 1; 40 | string Name = 2; 41 | commonpb.Address Address = 3; 42 | Menu Menu = 4; 43 | } 44 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/replies.go: -------------------------------------------------------------------------------- 1 | package restaurantapi 2 | 3 | import ( 4 | "github.com/stackus/edat/core" 5 | ) 6 | 7 | func registerReplies() { 8 | core.RegisterReplies(MenuItemsValidated{}) 9 | } 10 | 11 | type MenuItemsValidated struct { 12 | RestaurantID string 13 | MenuItems map[string]struct { 14 | Name string 15 | Price int 16 | } 17 | } 18 | 19 | func (MenuItemsValidated) ReplyName() string { return "restaurantapi.MenuItemsValidated" } 20 | -------------------------------------------------------------------------------- /serviceapis/restaurantapi/spec.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Address: 4 | type: object 5 | required: [ street1, city, state, zip ] 6 | properties: 7 | street1: 8 | type: string 9 | street2: 10 | type: string 11 | city: 12 | type: string 13 | state: 14 | type: string 15 | zip: 16 | type: string 17 | MenuItem: 18 | type: object 19 | required: [ id, name, price ] 20 | properties: 21 | id: 22 | type: string 23 | name: 24 | type: string 25 | price: 26 | type: integer 27 | -------------------------------------------------------------------------------- /shared-go/egress/options.go: -------------------------------------------------------------------------------- 1 | package egress 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type WaiterOption func(*waiterCfg) 8 | 9 | func WithSignals(signals ...os.Signal) WaiterOption { 10 | return func(cfg *waiterCfg) { 11 | cfg.signals = signals 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shared-go/instrumentation/message.go: -------------------------------------------------------------------------------- 1 | package instrumentation 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | "github.com/stackus/edat/msg" 10 | ) 11 | 12 | func MessageInstrumentation() func(msg.MessageReceiver) msg.MessageReceiver { 13 | responseTime := promauto.NewHistogram(prometheus.HistogramOpts{ 14 | Name: "message_response_time", 15 | Help: "Message response time in microseconds", 16 | Buckets: []float64{300, 600, 900, 1_500, 5_000, 10_000, 20_000}, 17 | }) 18 | 19 | return func(next msg.MessageReceiver) msg.MessageReceiver { 20 | return msg.ReceiveMessageFunc(func(ctx context.Context, message msg.Message) error { 21 | start := time.Now() 22 | err := next.ReceiveMessage(ctx, message) 23 | responseTime.Observe(float64(time.Since(start).Microseconds())) 24 | 25 | return err 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shared-go/instrumentation/web.go: -------------------------------------------------------------------------------- 1 | package instrumentation 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | ) 10 | 11 | func WebInstrumentation() func(http.Handler) http.Handler { 12 | responseTime := promauto.NewHistogram(prometheus.HistogramOpts{ 13 | Name: "web_response_time", 14 | Help: "Web response time in milliseconds", 15 | Buckets: []float64{300, 600, 900, 1_500, 5_000, 10_000, 20_000}, 16 | }) 17 | 18 | return func(next http.Handler) http.Handler { 19 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 20 | start := time.Now() 21 | next.ServeHTTP(writer, request) 22 | responseTime.Observe(float64(time.Since(start).Milliseconds())) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared-go/rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | ) 6 | 7 | type ClientCfg struct { 8 | CertPath string `envconfig:"CERT_PATH"` 9 | KeyPath string `envconfig:"KEY_PATH"` 10 | } 11 | 12 | func NewClientConn(cfg ClientCfg, uri string, options ...ClientOption) (*grpc.ClientConn, error) { 13 | clientCfg := &clientConfig{} 14 | 15 | if cfg.KeyPath != "" && cfg.CertPath != "" { 16 | // TODO secure/mutual auth connections 17 | } else { 18 | clientCfg.AddOption(grpc.WithInsecure()) 19 | } 20 | 21 | for _, option := range options { 22 | option(clientCfg) 23 | } 24 | 25 | return grpc.Dial(uri, clientCfg.ClientOptions()...) 26 | } 27 | -------------------------------------------------------------------------------- /shared-go/web/context.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | type contextKey struct { 11 | name string 12 | } 13 | 14 | func (k *contextKey) String() string { 15 | return "rest context value " + k.name 16 | } 17 | 18 | func NewContextKey(key string) *contextKey { 19 | return &contextKey{key} 20 | } 21 | 22 | func RequestCtxValue(key string) (func(next http.Handler) http.Handler, func(r *http.Request) string) { 23 | ctxKey := contextKey{key} 24 | 25 | set := func(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 27 | ctxValue := chi.URLParam(request, key) 28 | 29 | next.ServeHTTP(writer, request.WithContext(context.WithValue(request.Context(), ctxKey, ctxValue))) 30 | }) 31 | } 32 | get := func(r *http.Request) string { 33 | val := r.Context().Value(ctxKey) 34 | 35 | if val == nil { 36 | return "" 37 | } 38 | 39 | return val.(string) 40 | } 41 | 42 | return set, get 43 | } 44 | -------------------------------------------------------------------------------- /shared-go/web/error_response.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-chi/render" 8 | "github.com/stackus/errors" 9 | ) 10 | 11 | type ErrorResponse struct { 12 | StatusCode int `json:"-"` 13 | Message string `json:"message"` 14 | Err error `json:"-"` 15 | } 16 | 17 | func NewErrorResponse(err error) render.Renderer { 18 | if dErr, ok := err.(errors.Error); ok { 19 | return &ErrorResponse{ 20 | StatusCode: dErr.HTTPCode(), 21 | Message: fmt.Sprintf("%s: %s", errors.TypeCode(err), err.Error()), 22 | Err: dErr, 23 | } 24 | } 25 | return &ErrorResponse{ 26 | StatusCode: http.StatusBadRequest, 27 | Message: err.Error(), 28 | Err: err, 29 | } 30 | } 31 | 32 | func (e *ErrorResponse) Render(w http.ResponseWriter, r *http.Request) error { 33 | render.Status(r, e.StatusCode) 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /shared-go/web/spec.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | responses: 3 | ErrorResponse: 4 | description: Unexpected Error 5 | content: 6 | application/json: 7 | schema: 8 | type: object 9 | required: [ message ] 10 | properties: 11 | message: 12 | type: string -------------------------------------------------------------------------------- /store-web/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackus/ftgogo/store-web 2 | 3 | go 1.16 4 | 5 | replace github.com/stackus/ftgogo/serviceapis => ./../serviceapis 6 | 7 | replace shared-go => ../shared-go 8 | 9 | // Development replacements 10 | //replace github.com/stackus/edat => ../../edat 11 | //replace github.com/stackus/edat-msgpack => ../../edat-msgpack 12 | //replace github.com/stackus/edat-pgx => ../../edat-pgx 13 | 14 | require ( 15 | github.com/deepmap/oapi-codegen v1.8.2 16 | github.com/getkin/kin-openapi v0.68.0 17 | github.com/go-chi/chi/v5 v5.0.3 18 | github.com/go-chi/render v1.0.1 19 | github.com/stackus/ftgogo/serviceapis v0.0.0-20210116185538-3dd9fbb69179 20 | shared-go v0.0.0-00010101000000-000000000000 21 | ) 22 | -------------------------------------------------------------------------------- /store-web/internal/adapters/accounting_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/domain" 7 | ) 8 | 9 | type ( 10 | FindAccount struct { 11 | AccountID string 12 | } 13 | 14 | EnableAccount struct { 15 | AccountID string 16 | } 17 | 18 | DisableAccount EnableAccount 19 | ) 20 | 21 | type AccountingRepository interface { 22 | Find(context.Context, FindAccount) (*domain.Account, error) 23 | Enable(context.Context, EnableAccount) error 24 | Disable(context.Context, DisableAccount) error 25 | } 26 | -------------------------------------------------------------------------------- /store-web/internal/adapters/consumer_grpc_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/consumerapi/pb" 7 | "github.com/stackus/ftgogo/store-web/internal/domain" 8 | ) 9 | 10 | type ConsumerGRPCRepository struct { 11 | client consumerpb.ConsumerServiceClient 12 | } 13 | 14 | var _ ConsumerRepository = (*ConsumerGRPCRepository)(nil) 15 | 16 | func NewConsumerGrpcRepository(client consumerpb.ConsumerServiceClient) *ConsumerGRPCRepository { 17 | return &ConsumerGRPCRepository{client: client} 18 | } 19 | 20 | func (r ConsumerGRPCRepository) Find(ctx context.Context, findConsumer FindConsumer) (*domain.Consumer, error) { 21 | resp, err := r.client.GetConsumer(ctx, &consumerpb.GetConsumerRequest{ConsumerID: findConsumer.ConsumerID}) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return &domain.Consumer{ 27 | ConsumerID: resp.ConsumerID, 28 | Name: resp.Name, 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /store-web/internal/adapters/consumer_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | "github.com/stackus/ftgogo/store-web/internal/domain" 8 | ) 9 | 10 | type ( 11 | RegisterConsumer struct { 12 | Name string 13 | } 14 | 15 | FindConsumer struct { 16 | ConsumerID string 17 | } 18 | 19 | UpdateConsumer struct { 20 | Consumer domain.Consumer 21 | } 22 | 23 | AddConsumerAddress struct { 24 | ConsumerID string 25 | AddressID string 26 | Address *commonapi.Address 27 | } 28 | 29 | FindConsumerAddress struct { 30 | ConsumerID string 31 | AddressID string 32 | } 33 | 34 | UpdateConsumerAddress AddConsumerAddress 35 | RemoveConsumerAddress FindConsumerAddress 36 | ) 37 | 38 | type ConsumerRepository interface { 39 | Find(ctx context.Context, findConsumer FindConsumer) (*domain.Consumer, error) 40 | } 41 | -------------------------------------------------------------------------------- /store-web/internal/adapters/delivery_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/domain" 7 | ) 8 | 9 | type ( 10 | FindDelivery struct { 11 | DeliveryID string 12 | } 13 | 14 | FindCourier struct { 15 | CourierID string 16 | } 17 | 18 | SetCourierAvailability struct { 19 | CourierID string 20 | Available bool 21 | } 22 | ) 23 | 24 | type DeliveryRepository interface { 25 | FindDelivery(context.Context, FindDelivery) (*domain.Delivery, error) 26 | FindCourier(context.Context, FindCourier) (*domain.Courier, error) 27 | SetCourierAvailability(context.Context, SetCourierAvailability) error 28 | } 29 | -------------------------------------------------------------------------------- /store-web/internal/adapters/order_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stackus/ftgogo/serviceapis/commonapi" 8 | "github.com/stackus/ftgogo/serviceapis/orderapi" 9 | "github.com/stackus/ftgogo/store-web/internal/domain" 10 | ) 11 | 12 | type ( 13 | CreateOrder struct { 14 | ConsumerID string 15 | RestaurantID string 16 | DeliverAt time.Time 17 | DeliverTo *commonapi.Address 18 | LineItems commonapi.MenuItemQuantities 19 | } 20 | 21 | FindOrder struct { 22 | OrderID string 23 | } 24 | 25 | CancelOrder FindOrder 26 | 27 | ReviseOrder struct { 28 | OrderID string 29 | RevisedQuantities commonapi.MenuItemQuantities 30 | } 31 | ) 32 | 33 | type OrderRepository interface { 34 | Find(ctx context.Context, findOrder FindOrder) (*domain.Order, error) 35 | Cancel(ctx context.Context, cancelOrder CancelOrder) (orderapi.OrderState, error) 36 | } 37 | -------------------------------------------------------------------------------- /store-web/internal/adapters/restaurant_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 8 | "github.com/stackus/ftgogo/store-web/internal/domain" 9 | ) 10 | 11 | type ( 12 | CreateRestaurant struct { 13 | Name string 14 | Address *commonapi.Address 15 | MenuItems []restaurantapi.MenuItem 16 | } 17 | 18 | FindRestaurant struct { 19 | RestaurantID string 20 | } 21 | ) 22 | 23 | type RestaurantRepository interface { 24 | Create(context.Context, CreateRestaurant) (string, error) 25 | Find(ctx context.Context, restaurant FindRestaurant) (*domain.Restaurant, error) 26 | } 27 | -------------------------------------------------------------------------------- /store-web/internal/application/commands/cancel_order.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/orderapi" 7 | "github.com/stackus/ftgogo/store-web/internal/adapters" 8 | ) 9 | 10 | type CancelOrder struct { 11 | OrderID string 12 | } 13 | 14 | type CancelOrderHandler struct { 15 | repo adapters.OrderRepository 16 | } 17 | 18 | func NewCancelOrderHandler(repo adapters.OrderRepository) CancelOrderHandler { 19 | return CancelOrderHandler{repo: repo} 20 | } 21 | 22 | func (h CancelOrderHandler) Handle(ctx context.Context, cmd CancelOrder) (orderapi.OrderState, error) { 23 | _, err := h.repo.Find(ctx, adapters.FindOrder{ 24 | OrderID: cmd.OrderID, 25 | }) 26 | if err != nil { 27 | return orderapi.UnknownOrderState, err 28 | } 29 | 30 | return h.repo.Cancel(ctx, adapters.CancelOrder{ 31 | OrderID: cmd.OrderID, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /store-web/internal/application/commands/create_restaurant.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 8 | "github.com/stackus/ftgogo/store-web/internal/adapters" 9 | ) 10 | 11 | type CreateRestaurant struct { 12 | Name string 13 | Address *commonapi.Address 14 | MenuItems []restaurantapi.MenuItem 15 | } 16 | 17 | type CreateRestaurantHandler struct { 18 | repo adapters.RestaurantRepository 19 | } 20 | 21 | func NewCreateRestaurantHandler(repo adapters.RestaurantRepository) CreateRestaurantHandler { 22 | return CreateRestaurantHandler{repo: repo} 23 | } 24 | 25 | func (h CreateRestaurantHandler) Handle(ctx context.Context, cmd CreateRestaurant) (string, error) { 26 | return h.repo.Create(ctx, adapters.CreateRestaurant{ 27 | Name: cmd.Name, 28 | Address: cmd.Address, 29 | MenuItems: cmd.MenuItems, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /store-web/internal/application/commands/disable_account.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | ) 8 | 9 | type DisableAccount struct { 10 | AccountID string 11 | } 12 | 13 | type DisableAccountHandler struct { 14 | repo adapters.AccountingRepository 15 | } 16 | 17 | func NewDisableAccountHandler(repo adapters.AccountingRepository) DisableAccountHandler { 18 | return DisableAccountHandler{repo: repo} 19 | } 20 | 21 | func (h DisableAccountHandler) Handle(ctx context.Context, cmd DisableAccount) error { 22 | return h.repo.Disable(ctx, adapters.DisableAccount{AccountID: cmd.AccountID}) 23 | } 24 | -------------------------------------------------------------------------------- /store-web/internal/application/commands/enable_account.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | ) 8 | 9 | type EnableAccount struct { 10 | AccountID string 11 | } 12 | 13 | type EnableAccountHandler struct { 14 | repo adapters.AccountingRepository 15 | } 16 | 17 | func NewEnableAccountHandler(repo adapters.AccountingRepository) EnableAccountHandler { 18 | return EnableAccountHandler{repo: repo} 19 | } 20 | 21 | func (h EnableAccountHandler) Handle(ctx context.Context, cmd EnableAccount) error { 22 | return h.repo.Enable(ctx, adapters.EnableAccount{AccountID: cmd.AccountID}) 23 | } 24 | -------------------------------------------------------------------------------- /store-web/internal/application/commands/set_courier_availability.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | ) 8 | 9 | type SetCourierAvailability struct { 10 | CourierID string 11 | Available bool 12 | } 13 | 14 | type SetCourierAvailabilityHandler struct { 15 | repo adapters.DeliveryRepository 16 | } 17 | 18 | func NewSetCourierAvailabilityHandler(repo adapters.DeliveryRepository) SetCourierAvailabilityHandler { 19 | return SetCourierAvailabilityHandler{repo: repo} 20 | } 21 | 22 | func (h SetCourierAvailabilityHandler) Handle(ctx context.Context, cmd SetCourierAvailability) error { 23 | return h.repo.SetCourierAvailability(ctx, adapters.SetCourierAvailability{ 24 | CourierID: cmd.CourierID, 25 | Available: cmd.Available, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /store-web/internal/application/queries/get_account.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | "github.com/stackus/ftgogo/store-web/internal/domain" 8 | ) 9 | 10 | type GetAccount struct { 11 | AccountID string 12 | } 13 | 14 | type GetAccountHandler struct { 15 | repo adapters.AccountingRepository 16 | } 17 | 18 | func NewGetAccountHandler(repo adapters.AccountingRepository) GetAccountHandler { 19 | return GetAccountHandler{repo: repo} 20 | } 21 | 22 | func (h GetAccountHandler) Handle(ctx context.Context, cmd GetAccount) (*domain.Account, error) { 23 | return h.repo.Find(ctx, adapters.FindAccount{ 24 | AccountID: cmd.AccountID, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /store-web/internal/application/queries/get_consumer.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | "github.com/stackus/ftgogo/store-web/internal/domain" 8 | ) 9 | 10 | type GetConsumer struct { 11 | ConsumerID string 12 | } 13 | 14 | type GetConsumerHandler struct { 15 | repo adapters.ConsumerRepository 16 | } 17 | 18 | func NewGetConsumerHandler(repo adapters.ConsumerRepository) GetConsumerHandler { 19 | return GetConsumerHandler{repo: repo} 20 | } 21 | 22 | func (h GetConsumerHandler) Handle(ctx context.Context, cmd GetConsumer) (*domain.Consumer, error) { 23 | return h.repo.Find(ctx, adapters.FindConsumer{ 24 | ConsumerID: cmd.ConsumerID, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /store-web/internal/application/queries/get_order.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | "github.com/stackus/ftgogo/store-web/internal/domain" 8 | ) 9 | 10 | type GetOrder struct { 11 | OrderID string 12 | } 13 | 14 | type GetOrderHandler struct { 15 | repo adapters.OrderRepository 16 | } 17 | 18 | func NewGetOrderHandler(repo adapters.OrderRepository) GetOrderHandler { 19 | return GetOrderHandler{repo: repo} 20 | } 21 | 22 | func (h GetOrderHandler) Handle(ctx context.Context, query GetOrder) (*domain.Order, error) { 23 | return h.repo.Find(ctx, adapters.FindOrder{OrderID: query.OrderID}) 24 | } 25 | -------------------------------------------------------------------------------- /store-web/internal/application/queries/get_restaurant.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stackus/ftgogo/store-web/internal/adapters" 7 | "github.com/stackus/ftgogo/store-web/internal/domain" 8 | ) 9 | 10 | type GetRestaurant struct { 11 | RestaurantID string 12 | } 13 | 14 | type GetRestaurantHandler struct { 15 | repo adapters.RestaurantRepository 16 | } 17 | 18 | func NewGetRestaurantHandler(repo adapters.RestaurantRepository) GetRestaurantHandler { 19 | return GetRestaurantHandler{repo: repo} 20 | } 21 | 22 | func (h GetRestaurantHandler) Handle(ctx context.Context, query GetRestaurant) (*domain.Restaurant, error) { 23 | return h.repo.Find(ctx, adapters.FindRestaurant{RestaurantID: query.RestaurantID}) 24 | } 25 | -------------------------------------------------------------------------------- /store-web/internal/application/service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/store-web/internal/application/commands" 5 | "github.com/stackus/ftgogo/store-web/internal/application/queries" 6 | ) 7 | 8 | type Service struct { 9 | Commands Commands 10 | Queries Queries 11 | } 12 | 13 | type Commands struct { 14 | EnableAccount commands.EnableAccountHandler 15 | DisableAccount commands.DisableAccountHandler 16 | SetCourierAvailability commands.SetCourierAvailabilityHandler 17 | CancelOrder commands.CancelOrderHandler 18 | CreateRestaurant commands.CreateRestaurantHandler 19 | } 20 | 21 | type Queries struct { 22 | GetAccount queries.GetAccountHandler 23 | GetConsumer queries.GetConsumerHandler 24 | GetDeliveryHistory queries.GetDeliveryHistoryHandler 25 | GetOrder queries.GetOrderHandler 26 | GetRestaurant queries.GetRestaurantHandler 27 | } 28 | -------------------------------------------------------------------------------- /store-web/internal/domain/account.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Account struct { 4 | AccountID string 5 | Enabled bool 6 | } 7 | -------------------------------------------------------------------------------- /store-web/internal/domain/consumer.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Consumer struct { 4 | ConsumerID string 5 | Name string 6 | // TODO Addresses map[string]consumerapi.Address ?? 7 | } 8 | -------------------------------------------------------------------------------- /store-web/internal/domain/courier.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | ) 8 | 9 | type ActionType string 10 | 11 | const ( 12 | PickUp ActionType = "PICKUP" 13 | DropOff ActionType = "DROPOFF" 14 | ) 15 | 16 | type Courier struct { 17 | CourierID string 18 | Plan Plan 19 | Available bool 20 | } 21 | 22 | type Plan []Action 23 | 24 | type Action struct { 25 | DeliveryID string 26 | ActionType ActionType 27 | Address *commonapi.Address 28 | When time.Time 29 | } 30 | 31 | func (a ActionType) String() string { 32 | return string(a) 33 | } 34 | 35 | func (p Plan) ActionsFor(deliveryID string) []Action { 36 | actions := []Action{} 37 | for _, action := range p { 38 | if action.IsFor(deliveryID) { 39 | actions = append(actions, action) 40 | } 41 | } 42 | 43 | return actions 44 | } 45 | 46 | func (a Action) IsFor(deliveryID string) bool { 47 | return a.DeliveryID == deliveryID 48 | } 49 | -------------------------------------------------------------------------------- /store-web/internal/domain/delivery.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stackus/ftgogo/serviceapis/commonapi" 7 | ) 8 | 9 | type DeliveryStatus int 10 | 11 | const ( 12 | DeliveryPending DeliveryStatus = iota 13 | DeliveryScheduled 14 | DeliveryCancelled 15 | ) 16 | 17 | type Delivery struct { 18 | DeliveryID string 19 | RestaurantID string 20 | AssignedCourierID string 21 | PickUpAddress *commonapi.Address 22 | DeliveryAddress *commonapi.Address 23 | Status DeliveryStatus 24 | PickUpTime time.Time 25 | ReadyBy time.Time 26 | } 27 | 28 | func (s DeliveryStatus) String() string { 29 | switch s { 30 | case DeliveryPending: 31 | return "PENDING" 32 | case DeliveryScheduled: 33 | return "SCHEDULED" 34 | case DeliveryCancelled: 35 | return "CANCELLED" 36 | default: 37 | return "UNKNOWN" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /store-web/internal/domain/delivery_history.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type DeliveryHistory struct { 4 | ID string 5 | AssignedCourier string 6 | CourierActions []string 7 | Status string 8 | } 9 | -------------------------------------------------------------------------------- /store-web/internal/domain/order.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/serviceapis/orderapi" 5 | ) 6 | 7 | type Order struct { 8 | OrderID string 9 | ConsumerID string 10 | RestaurantID string 11 | RestaurantName string 12 | Total int 13 | Status orderapi.OrderState 14 | // TODO EstimatedDelivery time.Duration 15 | // TODO other data 16 | } 17 | -------------------------------------------------------------------------------- /store-web/internal/domain/restaurant.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/stackus/ftgogo/serviceapis/commonapi" 5 | "github.com/stackus/ftgogo/serviceapis/restaurantapi" 6 | ) 7 | 8 | type Restaurant struct { 9 | RestaurantID string 10 | Name string 11 | Address *commonapi.Address 12 | MenuItems []restaurantapi.MenuItem 13 | } 14 | -------------------------------------------------------------------------------- /store-web/internal/handlers/oapi-codegen.cfg.yaml: -------------------------------------------------------------------------------- 1 | output: web_server_api.gen.go 2 | package: handlers 3 | generate: 4 | - types 5 | - chi-server 6 | - spec 7 | import-mapping: 8 | ../../../serviceapis/commonapi/spec.yaml: github.com/stackus/ftgogo/serviceapis/commonapi 9 | --------------------------------------------------------------------------------