├── .replit ├── images └── janio-clean-code-arch.jpg ├── pkg ├── domain │ ├── repository │ │ ├── user_repository.go │ │ ├── product_repository.go │ │ └── cart_repository.go │ ├── entity │ │ ├── product.go │ │ ├── user.go │ │ └── cart.go │ ├── valueobject │ │ ├── cart_item.go │ │ └── buyer_address.go │ └── aggregate │ │ ├── user_cart.go │ │ └── user_cart_test.go ├── adapter │ └── repository │ │ └── inmem │ │ ├── model │ │ ├── product.go │ │ ├── cart.go │ │ └── user.go │ │ ├── product_repository.go │ │ ├── user_repository.go │ │ └── cart_repository.go ├── sharedkernel │ ├── enum │ │ ├── address_type.go │ │ └── cart_status.go │ ├── mock │ │ └── repository │ │ │ ├── user_repository.go │ │ │ ├── product_repository.go │ │ │ └── cart_repository.go │ └── error │ │ └── error_type.go └── usecase │ ├── users │ ├── user_ioport.go │ ├── user_interactor.go │ └── user_interactor_test.go │ ├── carts │ ├── cart_ioport.go │ ├── cart_interactor.go │ └── cart_interactor_test.go │ └── products │ ├── product_interactor.go │ └── product_interactor_test.go ├── go.mod ├── README.md ├── go.sum └── cmd └── cli └── main.go /.replit: -------------------------------------------------------------------------------- 1 | language = "go" 2 | run = "go run cmd/cli/main.go" -------------------------------------------------------------------------------- /images/janio-clean-code-arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yauritux/clean-code-architecture/HEAD/images/janio-clean-code-arch.jpg -------------------------------------------------------------------------------- /pkg/domain/repository/user_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type UserRepository interface { 4 | FindByUserID(string) (interface{}, error) 5 | } 6 | -------------------------------------------------------------------------------- /pkg/domain/repository/product_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type ProductRepository interface { 4 | FindByProductID(string) (interface{}, error) 5 | } 6 | -------------------------------------------------------------------------------- /pkg/domain/entity/product.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Product struct { 4 | ID string 5 | Name string 6 | Stock int 7 | Price float64 8 | Disc float64 9 | } 10 | -------------------------------------------------------------------------------- /pkg/adapter/repository/inmem/model/product.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Product struct { 4 | ID string 5 | Name string 6 | Stock int 7 | Price float64 8 | Disc float64 9 | } 10 | -------------------------------------------------------------------------------- /pkg/domain/valueobject/cart_item.go: -------------------------------------------------------------------------------- 1 | package valueobject 2 | 3 | type CartItem struct { 4 | ProdID string 5 | ProdName string 6 | Qty int 7 | Price float64 8 | Disc float64 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yauritux/cartsvc 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/lucsky/cuid v1.0.2 7 | github.com/smartystreets/goconvey v1.6.4 8 | github.com/stretchr/testify v1.5.1 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/sharedkernel/enum/address_type.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type AddressType string 4 | 5 | const ( 6 | BillingAddress AddressType = "billing_address" 7 | ShippingAddress AddressType = "shipping_address" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/usecase/users/user_ioport.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | type UserInputPort interface { 4 | FetchCurrentUser(id string) (interface{}, error) 5 | BuildUserUsecaseModel(interface{}) *User 6 | } 7 | 8 | type UserOutputPort interface { 9 | } 10 | -------------------------------------------------------------------------------- /pkg/sharedkernel/enum/cart_status.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type CartStatus string 4 | 5 | const ( 6 | Open CartStatus = "open" 7 | PaymentProcessing CartStatus = "payment_processing" 8 | Canceled CartStatus = "canceled" 9 | Closed CartStatus = "closed" 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/usecase/carts/cart_ioport.go: -------------------------------------------------------------------------------- 1 | package carts 2 | 3 | type CartInputPort interface { 4 | FetchUserCart(userID string) (interface{}, error) 5 | AddToCart(userID string, item interface{}) error 6 | } 7 | 8 | type CartOutputPort interface { 9 | BuildCartItemRepositoryModel(*CartItem) interface{} 10 | } 11 | -------------------------------------------------------------------------------- /pkg/domain/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | vo "github.com/yauritux/cartsvc/pkg/domain/valueobject" 5 | ) 6 | 7 | type User struct { 8 | UserID string 9 | Username string 10 | Phone string 11 | Email string 12 | BillingAddress vo.BuyerAddress 13 | ShippingAddress vo.BuyerAddress 14 | } 15 | -------------------------------------------------------------------------------- /pkg/domain/valueobject/buyer_address.go: -------------------------------------------------------------------------------- 1 | package valueobject 2 | 3 | import ( 4 | . "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 5 | ) 6 | 7 | type BuyerAddress struct { 8 | StreetName string 9 | City string 10 | Postal string 11 | Province string 12 | Region string 13 | Country string 14 | Type AddressType 15 | } 16 | -------------------------------------------------------------------------------- /pkg/domain/entity/cart.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | vo "github.com/yauritux/cartsvc/pkg/domain/valueobject" 7 | . "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 8 | ) 9 | 10 | type Cart struct { 11 | ID string 12 | UserID string 13 | Status CartStatus 14 | Items []*vo.CartItem 15 | CreatedAt time.Time 16 | CanceledAt *time.Time 17 | } 18 | -------------------------------------------------------------------------------- /pkg/domain/repository/cart_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type CartRepository interface { 4 | FetchUserCart(userID string) (interface{}, error) 5 | AddToCart(cartID string, item interface{}) error 6 | RemoveItem(cartID string, itemID string) error 7 | UpdateItem(cartID string, item interface{}) error 8 | Checkout(cartID string) interface{} 9 | Canceled(cartID string) error 10 | Close(cartID string) error 11 | } 12 | -------------------------------------------------------------------------------- /pkg/sharedkernel/mock/repository/user_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | type MockUserRepository struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockUserRepository) FindByUserID(uid string) (interface{}, error) { 12 | call := m.Called(uid) 13 | res := call.Get(0) 14 | if res == nil { 15 | return nil, call.Error(1) 16 | } 17 | return res, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/sharedkernel/mock/repository/product_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | type MockProductRepository struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockProductRepository) FindByProductID(id string) (interface{}, error) { 12 | call := m.Called(id) 13 | res := call.Get(0) 14 | if res == nil { 15 | return nil, call.Error(1) 16 | } 17 | return res, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/adapter/repository/inmem/model/cart.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | . "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 5 | 6 | "time" 7 | ) 8 | 9 | type Cart struct { 10 | ID string 11 | UserID string 12 | Status CartStatus 13 | Items []*CartItem 14 | CreatedAt time.Time 15 | CanceledAt *time.Time 16 | } 17 | 18 | type CartItem struct { 19 | ID string 20 | Name string 21 | Qty int 22 | Price float64 23 | Disc float64 24 | } 25 | -------------------------------------------------------------------------------- /pkg/adapter/repository/inmem/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import . "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 4 | 5 | type User struct { 6 | ID string 7 | Name string 8 | Phone string 9 | Email string 10 | BillingAddress *Address 11 | ShippingAddress *Address 12 | } 13 | 14 | type Address struct { 15 | StreetName string 16 | City string 17 | Postal string 18 | Province string 19 | Region string 20 | Country string 21 | AddressType AddressType 22 | } 23 | -------------------------------------------------------------------------------- /pkg/sharedkernel/error/error_type.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | type ErrNoData struct { 4 | message string 5 | } 6 | 7 | func NewErrNoData(msg string) *ErrNoData { 8 | return &ErrNoData{msg} 9 | } 10 | 11 | func (e *ErrNoData) Error() string { 12 | return e.message 13 | } 14 | 15 | type ErrConversion struct { 16 | message string 17 | } 18 | 19 | func NewErrConversion(msg string) *ErrConversion { 20 | return &ErrConversion{msg} 21 | } 22 | 23 | func (e *ErrConversion) Error() string { 24 | return e.message 25 | } 26 | 27 | type ErrDuplicateData struct { 28 | message string 29 | } 30 | 31 | func NewErrDuplicateData(msg string) *ErrDuplicateData { 32 | return &ErrDuplicateData{msg} 33 | } 34 | 35 | func (e *ErrDuplicateData) Error() string { 36 | return e.message 37 | } 38 | -------------------------------------------------------------------------------- /pkg/usecase/products/product_interactor.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yauritux/cartsvc/pkg/domain/repository" 7 | ) 8 | 9 | type ProductUsecase struct { 10 | repo repository.ProductRepository 11 | } 12 | 13 | type Product struct { 14 | ID string 15 | Name string 16 | Stock int 17 | Price float64 18 | Disc float64 19 | } 20 | 21 | func NewProductUsecase(r repository.ProductRepository) *ProductUsecase { 22 | return &ProductUsecase{r} 23 | } 24 | 25 | func (prod *ProductUsecase) FindByProductID(id string) (interface{}, error) { 26 | p, err := prod.repo.FindByProductID(id) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | productFound, ok := p.(*Product) 32 | if !ok { 33 | return nil, fmt.Errorf("cannot find product with ID %s, got an invalid product type returned from the repository", id) 34 | } 35 | 36 | return productFound, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/usecase/users/user_interactor.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/yauritux/cartsvc/pkg/domain/repository" 7 | . "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 8 | ) 9 | 10 | type UserUsecase struct { 11 | repo repository.UserRepository 12 | } 13 | 14 | type User struct { 15 | ID string 16 | Username string 17 | Email string 18 | Phone string 19 | BillingAddr *Address 20 | ShippingAddr *Address 21 | } 22 | 23 | type Address struct { 24 | Street string 25 | City string 26 | Postal string 27 | Province string 28 | Region string 29 | Country string 30 | AddressType AddressType 31 | } 32 | 33 | func NewUserUsecase(r repository.UserRepository) *UserUsecase { 34 | return &UserUsecase{r} 35 | } 36 | 37 | func (user *UserUsecase) FetchCurrentUser(id string) (interface{}, error) { 38 | u, err := user.repo.FindByUserID(id) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | currUser, ok := u.(*User) 44 | if !ok { 45 | return nil, errors.New("cannot fetch current user, got an invalid user type returned from the repository") 46 | } 47 | 48 | return currUser, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/sharedkernel/mock/repository/cart_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | type MockCartRepository struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockCartRepository) FetchUserCart(userID string) (interface{}, error) { 12 | call := m.Called(userID) 13 | res := call.Get(0) 14 | if res == nil { 15 | return nil, call.Error(1) 16 | } 17 | return res, nil 18 | } 19 | 20 | func (m *MockCartRepository) AddToCart(cartID string, item interface{}) error { 21 | call := m.Called(cartID, item) 22 | return call.Error(0) 23 | } 24 | 25 | func (m *MockCartRepository) RemoveItem(cartID string, itemID string) error { 26 | call := m.Called(cartID, itemID) 27 | return call.Error(0) 28 | } 29 | 30 | func (m *MockCartRepository) UpdateItem(cartID string, item interface{}) error { 31 | call := m.Called(cartID, item) 32 | return call.Error(0) 33 | } 34 | 35 | func (m *MockCartRepository) Checkout(cartID string) interface{} { 36 | call := m.Called(cartID) 37 | return call.Get(0) 38 | } 39 | 40 | func (m *MockCartRepository) Canceled(cartID string) error { 41 | call := m.Called(cartID) 42 | return call.Error(0) 43 | } 44 | 45 | func (m *MockCartRepository) Close(cartID string) error { 46 | call := m.Called(cartID) 47 | return call.Error(0) 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Run on Repl.it](https://repl.it/badge/github/yauritux/clean-code-architecture)](https://repl.it/github/yauritux/clean-code-architecture) 2 | 3 | # clean-code-architecture 4 | My personal opinion on how the Clean Code Architecture implementation looks like in `Go` within the context of the `Domain Driven Design` (DDD), yet i adopted some terms from the Onion Architecture such as `domain` in order to avoid any misleading interpretations with the `Entity` in `Domain Driven Design`. 5 | 6 | I'm using `in-memory` repository for the current implementation, however... will be adding another sample for other repository implementations such as `database` and `web services`. 7 | The client is also provided as a CLI application at this moment merely to show how this kinda architecture works. Absolutely, will be updating with other infrastructures and adapters such as REST APIs, gRPC services, etc. 8 | 9 | Here's what the code architecture looks like in the components level: 10 | 11 | ![JANIO HUB Backend Code Architecture](images/janio-clean-code-arch.jpg) 12 | 13 | ## Usage 14 | 15 | ### Run all Unit Tests 16 | 17 | From the terminal, execute this following command: 18 | 19 | `go test ./...` 20 | 21 | ### Test using CLI App 22 | 23 | From the terminal, execute this following command: 24 | 25 | `go run cmd/cli/main.go` 26 | 27 | ## Further Read 28 | 29 | - https://medium.com/@yauritux/ddd-part-5-b0caf2437912 30 | - https://dev.to/yauritux/clean-code-architecture-in-go-9fj 31 | -------------------------------------------------------------------------------- /pkg/adapter/repository/inmem/product_repository.go: -------------------------------------------------------------------------------- 1 | package inmem 2 | 3 | import ( 4 | "github.com/yauritux/cartsvc/pkg/adapter/repository/inmem/model" 5 | e "github.com/yauritux/cartsvc/pkg/sharedkernel/error" 6 | uc "github.com/yauritux/cartsvc/pkg/usecase/products" 7 | ) 8 | 9 | type ProductRepository struct { 10 | data []*model.Product 11 | } 12 | 13 | func NewProductRepository() *ProductRepository { 14 | productRecords := make([]*model.Product, 0) 15 | productRecords = append(productRecords, &model.Product{ 16 | ID: "001", 17 | Name: "Shuriken", 18 | Stock: 1500, 19 | Price: 250.50, 20 | Disc: 0.0, 21 | }) 22 | productRecords = append(productRecords, &model.Product{ 23 | ID: "002", 24 | Name: "Sai", 25 | Stock: 950, 26 | Price: 175.25, 27 | Disc: 0.0, 28 | }) 29 | return &ProductRepository{data: productRecords} 30 | } 31 | 32 | func (r *ProductRepository) FindByProductID(id string) (interface{}, error) { 33 | if id == "" { 34 | return nil, e.NewErrNoData("please provide product id") 35 | } 36 | for i, p := range r.data { 37 | if p.ID == id { 38 | return r.BuildProductUsecaseModel(r.data[i]), nil 39 | } 40 | } 41 | return nil, e.NewErrNoData("no product found for id " + id) 42 | } 43 | 44 | func (r *ProductRepository) BuildProductUsecaseModel(prod interface{}) *uc.Product { 45 | switch prod.(type) { 46 | case *model.Product: 47 | u := prod.(*model.Product) 48 | return &uc.Product{ 49 | ID: u.ID, 50 | Name: u.Name, 51 | Stock: u.Stock, 52 | Price: u.Price, 53 | Disc: u.Disc, 54 | } 55 | default: 56 | return nil 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/usecase/users/user_interactor_test.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/yauritux/cartsvc/pkg/domain/entity" 11 | mockUserRepo "github.com/yauritux/cartsvc/pkg/sharedkernel/mock/repository" 12 | ) 13 | 14 | func TestUserUsecase(t *testing.T) { 15 | 16 | Convey("1. When searching a user by ID", t, func() { 17 | 18 | userRepo := &mockUserRepo.MockUserRepository{} 19 | 20 | Convey("-> Negative Scenarios", func() { 21 | Convey("-> Some errors occures within the system repository", func() { 22 | Convey("-> Should return an error with a message related to a repository error", func() { 23 | userRepo.On("FindByUserID", mock.Anything).Return(nil, errors.New("Database error")) 24 | uc := NewUserUsecase(userRepo) 25 | res, err := uc.FetchCurrentUser(mock.Anything) 26 | So(res, ShouldBeNil) 27 | So(err, ShouldNotBeNil) 28 | So(err.Error(), ShouldEqual, "Database error") 29 | }) 30 | }) 31 | Convey("-> An error occured due to a wrong user system type", func() { 32 | Convey("-> Should return an error with a message of wrong user type", func() { 33 | userRepo.On("FindByUserID", mock.Anything).Return(&entity.User{}, nil) 34 | uc := NewUserUsecase(userRepo) 35 | res, err := uc.FetchCurrentUser(mock.Anything) 36 | So(res, ShouldBeNil) 37 | So(err, ShouldNotBeNil) 38 | So(err.Error(), ShouldEqual, "cannot fetch current user, got an invalid user type returned from the repository") 39 | }) 40 | }) 41 | }) 42 | 43 | Convey("-> Positive Scenarios", func() { 44 | Convey("-> User is found", func() { 45 | Convey("-> Should return a user with no error", func() { 46 | sUser := &User{ 47 | ID: "123", 48 | Username: "yauritux", 49 | } 50 | userRepo.On("FindByUserID", "123").Return(sUser, nil) 51 | uc := NewUserUsecase(userRepo) 52 | res, err := uc.FetchCurrentUser("123") 53 | So(res, ShouldNotBeNil) 54 | So(err, ShouldBeNil) 55 | So(reflect.DeepEqual(sUser, res), ShouldBeTrue) 56 | }) 57 | }) 58 | }) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/usecase/products/product_interactor_test.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/yauritux/cartsvc/pkg/domain/entity" 11 | mockProductRepo "github.com/yauritux/cartsvc/pkg/sharedkernel/mock/repository" 12 | ) 13 | 14 | func TestProductUsecase(t *testing.T) { 15 | 16 | Convey("1. Given a user is searching for a product by ID", t, func() { 17 | 18 | prodRepo := &mockProductRepo.MockProductRepository{} 19 | 20 | Convey("-> Negative Scenarios", func() { 21 | Convey("-> Some errors occured within the system repository", func() { 22 | Convey("-> Should return an error with a message related to a repository error", func() { 23 | prodRepo.On("FindByProductID", mock.Anything).Return(nil, errors.New("Database error")) 24 | uc := NewProductUsecase(prodRepo) 25 | res, err := uc.FindByProductID("001") 26 | So(res, ShouldBeNil) 27 | So(err, ShouldNotBeNil) 28 | So(err.Error(), ShouldEqual, "Database error") 29 | }) 30 | }) 31 | Convey("-> Some errors occured due to invalid product system type", func() { 32 | Convey("-> Should return an error with an error message of invalid product system type", func() { 33 | prodRepo.On("FindByProductID", mock.Anything).Return(&entity.Product{}, nil) 34 | uc := NewProductUsecase(prodRepo) 35 | res, err := uc.FindByProductID("001") 36 | So(res, ShouldBeNil) 37 | So(err, ShouldNotBeNil) 38 | So(err.Error(), ShouldEqual, "cannot find product with ID 001, got an invalid product type returned from the repository") 39 | }) 40 | }) 41 | }) 42 | 43 | Convey("-> Positive Scenarios", func() { 44 | Convey("-> Found the product", func() { 45 | Convey("-> Should return the product to the user without no error", func() { 46 | sProduct := &Product{ 47 | ID: "001", 48 | Name: "Shuriken", 49 | Stock: 999, 50 | Price: 1250, 51 | Disc: 0, 52 | } 53 | prodRepo.On("FindByProductID", "001").Return(sProduct, nil) 54 | uc := NewProductUsecase(prodRepo) 55 | res, err := uc.FindByProductID("001") 56 | So(res, ShouldNotBeNil) 57 | So(err, ShouldBeNil) 58 | So(reflect.DeepEqual(res, sProduct), ShouldBeTrue) 59 | }) 60 | }) 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 4 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 5 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 6 | github.com/lucsky/cuid v1.0.2 h1:z4XlExeoderxoPj2/dxKOyPxe9RCOu7yNq9/XWxIUMQ= 7 | github.com/lucsky/cuid v1.0.2/go.mod h1:QaaJqckboimOmhRSJXSx/+IT+VTfxfPGSo/6mfgUfmE= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 11 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 12 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 13 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 14 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 17 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 22 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg= 23 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 26 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 27 | -------------------------------------------------------------------------------- /pkg/adapter/repository/inmem/user_repository.go: -------------------------------------------------------------------------------- 1 | package inmem 2 | 3 | import ( 4 | "github.com/yauritux/cartsvc/pkg/adapter/repository/inmem/model" 5 | uc "github.com/yauritux/cartsvc/pkg/usecase/users" 6 | ) 7 | 8 | type UserRepository struct { 9 | data []*model.User 10 | } 11 | 12 | func NewUserRepository() *UserRepository { 13 | userRecords := make([]*model.User, 0) 14 | user := &model.User{ 15 | ID: "yauritux", 16 | Name: "Yauri Attamimi", 17 | Phone: "+62822xxxxxx", 18 | Email: "yauritux@gmail.com", 19 | BillingAddress: &model.Address{ 20 | StreetName: "Kalibata Raya No.1", 21 | City: "Jakarta", 22 | Region: "South Jakarta", 23 | Province: "DKI Jakarta", 24 | Postal: "12750", 25 | Country: "Indonesia", 26 | AddressType: "billing_address", 27 | }, 28 | ShippingAddress: &model.Address{ 29 | StreetName: "Kalibata Raya No.1", 30 | City: "Jakarta", 31 | Region: "DKI Jakarta", 32 | Postal: "12750", 33 | Country: "Indonesia", 34 | AddressType: "shipping_address", 35 | }, 36 | } 37 | userRecords = append(userRecords, user) 38 | return &UserRepository{data: userRecords} 39 | } 40 | 41 | func (r *UserRepository) FindByUserID(uid string) (interface{}, error) { 42 | if uid == "" { 43 | return nil, nil 44 | } 45 | for i, u := range r.data { 46 | if u.ID != uid { 47 | continue 48 | } 49 | return r.BuildUserUsecaseModel(r.data[i]), nil 50 | } 51 | return nil, nil 52 | } 53 | 54 | func (r *UserRepository) BuildUserUsecaseModel(user interface{}) *uc.User { 55 | switch user.(type) { 56 | case *model.User: 57 | u := user.(*model.User) 58 | return &uc.User{ 59 | ID: u.ID, 60 | Username: u.Name, 61 | Phone: u.Phone, 62 | Email: u.Email, 63 | BillingAddr: &uc.Address{ 64 | Street: u.BillingAddress.StreetName, 65 | City: u.BillingAddress.City, 66 | Postal: u.BillingAddress.Postal, 67 | Province: u.BillingAddress.Province, 68 | Region: u.BillingAddress.Region, 69 | Country: u.BillingAddress.Country, 70 | AddressType: u.BillingAddress.AddressType, 71 | }, 72 | ShippingAddr: &uc.Address{ 73 | Street: u.ShippingAddress.StreetName, 74 | City: u.ShippingAddress.City, 75 | Postal: u.ShippingAddress.Postal, 76 | Province: u.ShippingAddress.Province, 77 | Region: u.ShippingAddress.Region, 78 | Country: u.ShippingAddress.Country, 79 | AddressType: u.ShippingAddress.AddressType, 80 | }, 81 | } 82 | default: 83 | return nil 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/yauritux/cartsvc/pkg/adapter/repository/inmem" 11 | cartSvc "github.com/yauritux/cartsvc/pkg/usecase/carts" 12 | productSvc "github.com/yauritux/cartsvc/pkg/usecase/products" 13 | ) 14 | 15 | var prodUsecase *productSvc.ProductUsecase 16 | var cartRepository *inmem.CartRepository 17 | var cartUsecase *cartSvc.CartUsecase 18 | 19 | func init() { 20 | prodRepository := inmem.NewProductRepository() 21 | cartRepository = inmem.NewCartRepository("yauritux") 22 | prodUsecase = productSvc.NewProductUsecase(prodRepository) 23 | cartUsecase = cartSvc.NewCartUsecase(cartRepository, prodRepository) 24 | } 25 | 26 | func main() { 27 | scanner := bufio.NewScanner(os.Stdin) 28 | 29 | showMenu() 30 | 31 | for scanner.Scan() { 32 | text := scanner.Text() 33 | switch text { 34 | case "1": 35 | if err := addItemToCart(scanner); err != nil { 36 | fmt.Printf("failed to add item... %v\n", err) 37 | } 38 | fmt.Println() 39 | showMenu() 40 | case "2": 41 | if err := showCartItems(); err != nil { 42 | fmt.Printf("failed to show cart items...%v\n", err) 43 | } 44 | fmt.Println() 45 | showMenu() 46 | case "3": 47 | os.Exit(0) 48 | default: 49 | fmt.Println() 50 | showMenu() 51 | } 52 | } 53 | } 54 | 55 | func showMenu() { 56 | fmt.Println("1. Add item to cart") 57 | fmt.Println("2. Show items in cart") 58 | fmt.Println("3. Exit") 59 | fmt.Print("Your choice [1 or 3]: ") 60 | } 61 | 62 | func addItemToCart(r *bufio.Scanner) error { 63 | fmt.Print("Enter product ID: ") 64 | r.Scan() 65 | prodID := r.Text() 66 | prodFound, err := prodUsecase.FindByProductID(prodID) 67 | if err != nil { 68 | return err 69 | } 70 | prod, ok := prodFound.(*productSvc.Product) 71 | if !ok { 72 | return errors.New("failed to get product from the repository...invalid type of product usecase model") 73 | } 74 | 75 | fmt.Print("Enter amount: ") 76 | r.Scan() 77 | amt := r.Text() 78 | qty, err := strconv.Atoi(amt) 79 | if err != nil { 80 | return errors.New("invalid amount") 81 | } 82 | 83 | if err := cartUsecase.AddToCart("yauritux", &cartSvc.CartItem{ 84 | ID: prod.ID, 85 | Name: prod.Name, 86 | Qty: qty, 87 | Price: prod.Price, 88 | Disc: prod.Disc, 89 | }); err != nil { 90 | fmt.Println("error = ", err) 91 | return err 92 | } 93 | fmt.Printf("added %d item of %s to cart\n", qty, prod.Name) 94 | return nil 95 | } 96 | 97 | func showCartItems() error { 98 | userCart, err := cartUsecase.FetchUserCart("yauritux") 99 | if err != nil { 100 | return err 101 | } 102 | cart, ok := userCart.(*cartSvc.Cart) 103 | if !ok { 104 | return errors.New("failed to show cart items, invalid type of cart usecase model") 105 | } 106 | fmt.Println("\nYour cart items:") 107 | for _, v := range cart.Items { 108 | fmt.Printf("%d pcs of %s (%s)\n", v.Qty, v.ID, v.Name) 109 | } 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/domain/aggregate/user_cart.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/yauritux/cartsvc/pkg/domain/entity" 8 | vo "github.com/yauritux/cartsvc/pkg/domain/valueobject" 9 | "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 10 | e "github.com/yauritux/cartsvc/pkg/sharedkernel/error" 11 | ) 12 | 13 | //UserCart is represents aggregate which contains our core business logic 14 | //(enterprise business rules) for user's shopping cart 15 | type UserCart struct { 16 | user *entity.User 17 | cart *entity.Cart 18 | } 19 | 20 | func NewUserCart(user *entity.User, cart *entity.Cart) *UserCart { 21 | if cart.Status == "" { 22 | cart.Status = enum.Open 23 | } 24 | if cart.Items == nil { 25 | cart.Items = make([]*vo.CartItem, 0) 26 | } 27 | return &UserCart{ 28 | user: user, 29 | cart: cart, 30 | } 31 | } 32 | 33 | func (userCart *UserCart) AddItemToCart(prod *entity.Product, qty int) (*vo.CartItem, error) { 34 | if qty > prod.Stock { 35 | return nil, errors.New("out of stock") 36 | } 37 | addedItem := &vo.CartItem{ 38 | ProdID: prod.ID, 39 | ProdName: prod.Name, 40 | Qty: qty, 41 | Price: prod.Price, 42 | Disc: prod.Disc, 43 | } 44 | if err := userCart.Validate(); err != nil { 45 | return nil, err 46 | } 47 | if userCart.cart.Status != enum.Open { 48 | return nil, fmt.Errorf("cannot add item to a cart with status as %s", userCart.cart.Status) 49 | } 50 | 51 | qtyOverride := false 52 | for i, v := range userCart.cart.Items { 53 | if prod.ID == v.ProdID { 54 | userCart.cart.Items[i].Qty = userCart.cart.Items[i].Qty + qty 55 | addedItem.Qty = userCart.cart.Items[i].Qty 56 | userCart.cart.Items[i] = addedItem 57 | qtyOverride = true 58 | break 59 | } 60 | } 61 | 62 | if qtyOverride { 63 | return addedItem, e.NewErrDuplicateData("item exists, updated amount of cart existing item") 64 | } 65 | userCart.cart.Items = append(userCart.cart.Items, addedItem) 66 | return addedItem, nil 67 | } 68 | 69 | func (userCart *UserCart) UpdateItemInCart(item *vo.CartItem) error { 70 | if userCart.cart.Items == nil || len(userCart.cart.Items) == 0 { 71 | return e.NewErrNoData("cart is still empty") 72 | } 73 | itemUpdated := false 74 | for i, v := range userCart.cart.Items { 75 | if item.ProdID == v.ProdID { 76 | userCart.cart.Items[i] = item 77 | itemUpdated = true 78 | break 79 | } 80 | } 81 | if itemUpdated == false { 82 | return e.NewErrNoData(fmt.Sprintf( 83 | "cannot find cart item %s - %s within the cart", 84 | item.ProdID, item.ProdName, 85 | )) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (userCart *UserCart) RemoveItemFromCart(itemID string) error { 92 | if userCart.cart.Items == nil || len(userCart.cart.Items) == 0 { 93 | return e.NewErrNoData("cart is still empty") 94 | } 95 | 96 | updatedCartItems := make([]*vo.CartItem, 0) 97 | 98 | for i, v := range userCart.cart.Items { 99 | if v.ProdID == itemID { 100 | updatedCartItems = append(updatedCartItems, v) 101 | userCart.cart.Items = append(userCart.cart.Items[:i], userCart.cart.Items[i+1:]...) 102 | break 103 | } 104 | } 105 | 106 | if len(updatedCartItems) == 0 { 107 | return fmt.Errorf("cannot find cart item with ID %s", itemID) 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func (userCart *UserCart) FetchUserInfo() *entity.User { 114 | return userCart.user 115 | } 116 | 117 | func (userCart *UserCart) FetchCartInfo() *entity.Cart { 118 | return userCart.cart 119 | } 120 | 121 | func (userCart *UserCart) Validate() error { 122 | if userCart.cart.ID == "" { 123 | return errors.New("cart 'session_id' is missing") 124 | } 125 | if userCart.cart.UserID == "" { 126 | return errors.New("cart is orphaned (who's owning this cart ?)") 127 | } 128 | if userCart.user.UserID == "" { 129 | return errors.New("cart 'user_id' is missing") 130 | } 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /pkg/usecase/carts/cart_interactor.go: -------------------------------------------------------------------------------- 1 | package carts 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/yauritux/cartsvc/pkg/domain/aggregate" 8 | "github.com/yauritux/cartsvc/pkg/domain/entity" 9 | "github.com/yauritux/cartsvc/pkg/domain/repository" 10 | vo "github.com/yauritux/cartsvc/pkg/domain/valueobject" 11 | . "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 12 | e "github.com/yauritux/cartsvc/pkg/sharedkernel/error" 13 | prodUsecase "github.com/yauritux/cartsvc/pkg/usecase/products" 14 | ) 15 | 16 | type CartUsecase struct { 17 | cartRepo repository.CartRepository 18 | prodRepo repository.ProductRepository 19 | } 20 | 21 | type Cart struct { 22 | ID string 23 | UserID string 24 | Status CartStatus 25 | Items []*CartItem 26 | CreatedAt time.Time 27 | CanceledAt *time.Time 28 | } 29 | 30 | type CartItem struct { 31 | ID string 32 | Name string 33 | Qty int 34 | Price float64 35 | Disc float64 36 | } 37 | 38 | func NewCartUsecase(r1 repository.CartRepository, r2 repository.ProductRepository) *CartUsecase { 39 | return &CartUsecase{r1, r2} 40 | } 41 | 42 | func (this *CartUsecase) FetchUserCart(userID string) (interface{}, error) { 43 | if userID == "" { 44 | return nil, e.NewErrNoData("cannot fetch user cart, 'user_id' is missing") 45 | } 46 | 47 | cart, err := this.cartRepo.FetchUserCart(userID) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | ucCart, ok := cart.(*Cart) 53 | if !ok { 54 | return nil, e.NewErrConversion("cannot fetch user cart, invalid type of cart usecase model") 55 | } 56 | 57 | return ucCart, nil 58 | } 59 | 60 | func (this *CartUsecase) AddToCart(userID string, item interface{}) error { 61 | userCart, err := this.cartRepo.FetchUserCart(userID) 62 | if err != nil { 63 | return err 64 | } 65 | currentCart, ok := userCart.(*Cart) 66 | if !ok { 67 | return errors.New("conversion failed, invalid type of cart usecase model") 68 | } 69 | 70 | prodItem, ok := item.(*CartItem) 71 | if !ok { 72 | return errors.New("conversion failed, invalid type of product item usecase model") 73 | } 74 | 75 | product, err := this.prodRepo.FindByProductID(prodItem.ID) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | ucProduct, ok := product.(*prodUsecase.Product) 81 | if !ok { 82 | return errors.New("conversion failed, invalid type of product usecase model") 83 | } 84 | 85 | cart := aggregate.NewUserCart( 86 | &entity.User{ 87 | UserID: currentCart.UserID, 88 | }, &entity.Cart{ 89 | ID: currentCart.ID, 90 | UserID: currentCart.UserID, 91 | Status: currentCart.Status, 92 | Items: buildCartVOItems(currentCart.Items), 93 | CreatedAt: currentCart.CreatedAt, 94 | }) 95 | 96 | addedItem, err := cart.AddItemToCart(&entity.Product{ 97 | ID: ucProduct.ID, 98 | Name: ucProduct.Name, 99 | Stock: ucProduct.Stock, 100 | Price: ucProduct.Price, 101 | Disc: ucProduct.Disc, 102 | }, prodItem.Qty) 103 | if err != nil { 104 | switch err.(type) { 105 | case *e.ErrDuplicateData: 106 | return this.cartRepo.UpdateItem(cart.FetchCartInfo().ID, buildCartUsecaseItem(addedItem)) 107 | default: 108 | return err 109 | } 110 | } 111 | 112 | return this.cartRepo.AddToCart(cart.FetchCartInfo().ID, buildCartUsecaseItem(addedItem)) 113 | } 114 | 115 | func buildCartUsecaseItem(item interface{}) *CartItem { 116 | var ucCartItem *CartItem 117 | switch item.(type) { 118 | case *vo.CartItem: 119 | cartItem := item.(*vo.CartItem) 120 | ucCartItem = &CartItem{ 121 | ID: cartItem.ProdID, 122 | Name: cartItem.ProdName, 123 | Qty: cartItem.Qty, 124 | Price: cartItem.Price, 125 | Disc: cartItem.Disc, 126 | } 127 | } 128 | return ucCartItem 129 | } 130 | 131 | func buildCartVOItems(items interface{}) []*vo.CartItem { 132 | voCartItems := make([]*vo.CartItem, 0) 133 | switch items.(type) { 134 | case []*CartItem: 135 | cartItems := items.([]*CartItem) 136 | for _, v := range cartItems { 137 | voCartItems = append(voCartItems, &vo.CartItem{ 138 | ProdID: v.ID, 139 | ProdName: v.Name, 140 | Qty: v.Qty, 141 | Price: v.Price, 142 | Disc: v.Disc, 143 | }) 144 | } 145 | } 146 | return voCartItems 147 | } 148 | -------------------------------------------------------------------------------- /pkg/adapter/repository/inmem/cart_repository.go: -------------------------------------------------------------------------------- 1 | package inmem 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/lucsky/cuid" 9 | 10 | "github.com/yauritux/cartsvc/pkg/adapter/repository/inmem/model" 11 | uc "github.com/yauritux/cartsvc/pkg/usecase/carts" 12 | ) 13 | 14 | var carts []*model.Cart 15 | 16 | type CartRepository struct { 17 | currentCart *model.Cart 18 | } 19 | 20 | func NewCartRepository(uid string) *CartRepository { 21 | if carts == nil { 22 | carts = []*model.Cart{ 23 | newCart(uid), 24 | } 25 | return &CartRepository{carts[0]} 26 | } 27 | 28 | for i, v := range carts { 29 | if v.UserID == uid && v.Status == "open" { 30 | return &CartRepository{carts[i]} 31 | } 32 | } 33 | carts = append(carts, newCart(uid)) 34 | return &CartRepository{carts[len(carts)-1]} 35 | } 36 | 37 | func (r *CartRepository) FetchUserCart(userID string) (interface{}, error) { 38 | for i, v := range carts { 39 | if v.UserID == userID { 40 | return buildCartUsecaseModel(carts[i]), nil 41 | } 42 | } 43 | 44 | return nil, nil 45 | } 46 | 47 | func (r *CartRepository) AddToCart(cartID string, item interface{}) error { 48 | currUserCart, err := r.getCurrentUserCart(cartID) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if currUserCart.Status != "open" { 54 | return fmt.Errorf("cart with ID of %s is %s, please create a new cart", 55 | cartID, currUserCart.Status) 56 | } 57 | 58 | cartItem, ok := item.(*uc.CartItem) 59 | if !ok { 60 | return errors.New("failed to add item, invalid type of cart item") 61 | } 62 | 63 | currUserCart.Items = append(currUserCart.Items, r.BuildCartItemRepositoryModel(cartItem).(*model.CartItem)) 64 | r.currentCart.Items = currUserCart.Items 65 | return nil 66 | } 67 | 68 | func (r *CartRepository) RemoveItem(id string, itemID string) error { 69 | currUserCart, err := r.getCurrentUserCart(id) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | if currUserCart.Status != "open" { 75 | return fmt.Errorf("cart with ID of %s is %s, please create a new cart", 76 | id, currUserCart.Status) 77 | } 78 | 79 | var newCartItems []*model.CartItem 80 | 81 | for i, v := range currUserCart.Items { 82 | if v.ID != itemID { 83 | continue 84 | } 85 | newCartItems = make([]*model.CartItem, 0) 86 | newCartItems = append(newCartItems[:i], newCartItems[i+1:]...) 87 | currUserCart.Items = newCartItems 88 | } 89 | return nil 90 | } 91 | 92 | func (r *CartRepository) UpdateItem(id string, item interface{}) error { 93 | currUserCart, err := r.getCurrentUserCart(id) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if currUserCart.Status != "open" { 99 | return fmt.Errorf("cart with ID of %s is %s, please create a new cart", 100 | id, currUserCart.Status) 101 | } 102 | 103 | cartItem, ok := item.(*uc.CartItem) 104 | if !ok { 105 | return errors.New("failed to update item, invalid type of cart item") 106 | } 107 | 108 | updatedCartItem := r.BuildCartItemRepositoryModel(cartItem).(*model.CartItem) 109 | for i, v := range currUserCart.Items { 110 | if v.ID == updatedCartItem.ID { 111 | currUserCart.Items[i] = updatedCartItem 112 | break 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (r *CartRepository) Checkout(id string) interface{} { 120 | currUserCart, err := r.getCurrentUserCart(id) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if currUserCart.Status != "open" { 126 | return fmt.Errorf("failed to checkout cart with ID of %s, it is already in %s", 127 | id, currUserCart.Status) 128 | } 129 | 130 | return currUserCart 131 | } 132 | 133 | func (r *CartRepository) Canceled(id string) error { 134 | currUserCart, err := r.getCurrentUserCart(id) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | if currUserCart.Status != "open" { 140 | return fmt.Errorf("cannot cancel the cart with status of %s", currUserCart.Status) 141 | } 142 | 143 | currUserCart.Status = "canceled" 144 | return nil 145 | } 146 | 147 | func (r *CartRepository) Close(id string) error { 148 | currUserCart, err := r.getCurrentUserCart(id) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if currUserCart.Status != "payment_processing" { 154 | return fmt.Errorf("failed to close cart with ID of %s. It is %s, please settle the payment first", 155 | id, currUserCart.Status) 156 | } 157 | 158 | currUserCart.Status = "closed" 159 | 160 | return nil 161 | } 162 | 163 | func (r *CartRepository) BuildCartItemRepositoryModel(item *uc.CartItem) interface{} { 164 | return &model.CartItem{ 165 | ID: item.ID, 166 | Name: item.Name, 167 | Qty: item.Qty, 168 | Price: item.Price, 169 | Disc: item.Disc, 170 | } 171 | } 172 | 173 | func newCart(uid string) *model.Cart { 174 | return &model.Cart{ 175 | ID: cuid.New(), 176 | UserID: uid, 177 | Status: "open", 178 | Items: make([]*model.CartItem, 0), 179 | CreatedAt: time.Now(), 180 | } 181 | } 182 | 183 | func (r *CartRepository) getCurrentUserCart(cartID string) (*model.Cart, error) { 184 | for i, v := range carts { 185 | if v.ID == cartID { 186 | return carts[i], nil 187 | } 188 | } 189 | return nil, fmt.Errorf("cannot find cart with id %s", cartID) 190 | } 191 | 192 | func buildCartUsecaseModel(cart *model.Cart) *uc.Cart { 193 | ucCart := &uc.Cart{ 194 | ID: cart.ID, 195 | UserID: cart.UserID, 196 | Status: cart.Status, 197 | CreatedAt: cart.CreatedAt, 198 | } 199 | ucCartItems := make([]*uc.CartItem, 0) 200 | for _, v := range cart.Items { 201 | ucCartItems = append(ucCartItems, &uc.CartItem{ 202 | ID: v.ID, 203 | Name: v.Name, 204 | Qty: v.Qty, 205 | Price: v.Price, 206 | Disc: v.Disc, 207 | }) 208 | } 209 | ucCart.Items = ucCartItems 210 | return ucCart 211 | } 212 | -------------------------------------------------------------------------------- /pkg/usecase/carts/cart_interactor_test.go: -------------------------------------------------------------------------------- 1 | package carts 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/yauritux/cartsvc/pkg/domain/entity" 12 | vo "github.com/yauritux/cartsvc/pkg/domain/valueobject" 13 | "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 14 | mockRepo "github.com/yauritux/cartsvc/pkg/sharedkernel/mock/repository" 15 | prodUsecase "github.com/yauritux/cartsvc/pkg/usecase/products" 16 | ) 17 | 18 | func TestCartUsecase(t *testing.T) { 19 | 20 | Convey("1. Given a user get his cart", t, func() { 21 | 22 | cartRepo := &mockRepo.MockCartRepository{} 23 | prodRepo := &mockRepo.MockProductRepository{} 24 | 25 | Convey("-> Negative Scenarios", func() { 26 | Convey("-> When userID is empty, then should return an error", func() { 27 | uc := NewCartUsecase(cartRepo, prodRepo) 28 | res, err := uc.FetchUserCart("") 29 | So(res, ShouldBeNil) 30 | So(err, ShouldNotBeNil) 31 | So(err.Error(), ShouldEqual, "cannot fetch user cart, 'user_id' is missing") 32 | }) 33 | Convey("-> Should return an error for some errors occured within the system repository", func() { 34 | cartRepo.On("FetchUserCart", mock.Anything).Return(nil, errors.New("Database error")) 35 | uc := NewCartUsecase(cartRepo, prodRepo) 36 | res, err := uc.FetchUserCart("123") 37 | So(res, ShouldBeNil) 38 | So(err, ShouldNotBeNil) 39 | So(err.Error(), ShouldEqual, "Database error") 40 | }) 41 | Convey("-> Should return an error for invalid cart system type", func() { 42 | cartRepo.On("FetchUserCart", "123").Return(&entity.Cart{}, nil) 43 | uc := NewCartUsecase(cartRepo, prodRepo) 44 | res, err := uc.FetchUserCart("123") 45 | So(res, ShouldBeNil) 46 | So(err, ShouldNotBeNil) 47 | So(err.Error(), ShouldEqual, "cannot fetch user cart, invalid type of cart usecase model") 48 | }) 49 | }) 50 | 51 | Convey("-> Positive Scenarios", func() { 52 | Convey("-> Cart is found, should return the cart object with no error", func() { 53 | sCart := &Cart{ 54 | ID: "001", 55 | UserID: "123", 56 | Status: enum.Open, 57 | CreatedAt: time.Now(), 58 | } 59 | cartRepo.On("FetchUserCart", "123").Return(sCart, nil) 60 | uc := NewCartUsecase(cartRepo, prodRepo) 61 | res, err := uc.FetchUserCart("123") 62 | So(res, ShouldNotBeNil) 63 | So(err, ShouldBeNil) 64 | So(reflect.DeepEqual(res, sCart), ShouldBeTrue) 65 | }) 66 | }) 67 | }) 68 | 69 | Convey("2. Given a user add an item to his cart", t, func() { 70 | 71 | cartRepo := &mockRepo.MockCartRepository{} 72 | prodRepo := &mockRepo.MockProductRepository{} 73 | 74 | Convey("-> Negative Scenarios", func() { 75 | Convey("-> Returns an error when cart cannot be fetched due to errors occured in the system repository", func() { 76 | cartRepo.On("FetchUserCart", mock.Anything).Return(nil, errors.New("Database error")) 77 | uc := NewCartUsecase(cartRepo, prodRepo) 78 | err := uc.AddToCart("123", &entity.Cart{}) 79 | So(err, ShouldNotBeNil) 80 | So(err.Error(), ShouldEqual, "Database error") 81 | }) 82 | Convey("-> Returns an error when a wrong type of cart is returned by the system repository", func() { 83 | cartRepo.On("FetchUserCart", mock.Anything).Return(&entity.Cart{}, nil) 84 | uc := NewCartUsecase(cartRepo, prodRepo) 85 | err := uc.AddToCart("123", &entity.Cart{}) 86 | So(err, ShouldNotBeNil) 87 | So(err.Error(), ShouldEqual, "conversion failed, invalid type of cart usecase model") 88 | }) 89 | Convey("-> Returns an error when a wrong type of cart item is added", func() { 90 | cartRepo.On("FetchUserCart", "123").Return(&Cart{ 91 | ID: "123", UserID: "123", Status: enum.Open, CreatedAt: time.Now(), 92 | }, nil) 93 | uc := NewCartUsecase(cartRepo, prodRepo) 94 | err := uc.AddToCart("123", &vo.CartItem{}) 95 | So(err, ShouldNotBeNil) 96 | So(err.Error(), ShouldEqual, "conversion failed, invalid type of product item usecase model") 97 | }) 98 | Convey("-> Returns an error when failed to locate the item from the product repository", func() { 99 | cartRepo.On("FetchUserCart", "123").Return(&Cart{ 100 | ID: "123", UserID: "123", Status: enum.Open, CreatedAt: time.Now(), 101 | }, nil) 102 | prodRepo.On("FindByProductID", "001").Return(nil, errors.New("Product not found")) 103 | uc := NewCartUsecase(cartRepo, prodRepo) 104 | err := uc.AddToCart("123", &CartItem{ID: "001", Name: "Shuriken", Price: 1250}) 105 | So(err, ShouldNotBeNil) 106 | So(err.Error(), ShouldEqual, "Product not found") 107 | }) 108 | Convey("-> Return an error when cart item returned from the product repository is invalid", func() { 109 | cartRepo.On("FetchUserCart", "123").Return(&Cart{ 110 | ID: "123", UserID: "123", Status: enum.Open, CreatedAt: time.Now(), 111 | }, nil) 112 | prodRepo.On("FindByProductID", "001").Return(&entity.Product{}, nil) 113 | uc := NewCartUsecase(cartRepo, prodRepo) 114 | err := uc.AddToCart("123", &CartItem{ID: "001", Name: "Shuriken", Price: 1250}) 115 | So(err, ShouldNotBeNil) 116 | So(err.Error(), ShouldEqual, "conversion failed, invalid type of product usecase model") 117 | }) 118 | }) 119 | 120 | Convey("-> Positive Scenarios", func() { 121 | Convey("-> When everything goes normal, no error is thrown", func() { 122 | cartRepo.On("FetchUserCart", "123").Return(&Cart{ 123 | ID: "123", UserID: "123", Status: enum.Open, CreatedAt: time.Now(), 124 | }, nil) 125 | prodRepo.On("FindByProductID", "001").Return(&prodUsecase.Product{ 126 | ID: "001", Name: "Shuriken", Price: 1250, Stock: 999, Disc: 0, 127 | }, nil) 128 | cartRepo.On("AddToCart", mock.Anything, mock.Anything).Return(nil) 129 | uc := NewCartUsecase(cartRepo, prodRepo) 130 | err := uc.AddToCart("123", &CartItem{ID: "001", Name: "Shuriken", Price: 1250, Qty: 1}) 131 | So(err, ShouldBeNil) 132 | }) 133 | }) 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /pkg/domain/aggregate/user_cart_test.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/yauritux/cartsvc/pkg/domain/entity" 8 | vo "github.com/yauritux/cartsvc/pkg/domain/valueobject" 9 | "github.com/yauritux/cartsvc/pkg/sharedkernel/enum" 10 | 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | var u *entity.User 15 | var c *entity.Cart 16 | var p *entity.Product 17 | 18 | func setup() { 19 | u = &entity.User{ 20 | UserID: "yauritux", 21 | Username: "Yauri Attamimi", 22 | Phone: "+6282225251437", 23 | Email: "yauritux@gmail.com", 24 | BillingAddress: vo.BuyerAddress{ 25 | StreetName: "Kalibata Raya", 26 | City: "Jakarta", 27 | Postal: "12750", 28 | Province: "DKI Jakarta", 29 | Region: "South Jakarta", 30 | Country: "Indonesia", 31 | Type: enum.BillingAddress, 32 | }, 33 | ShippingAddress: vo.BuyerAddress{ 34 | StreetName: "Kalibata Raya", 35 | City: "Jakarta", 36 | Postal: "12750", 37 | Province: "DKI Jakarta", 38 | Region: "South Jakarta", 39 | Country: "Indonesia", 40 | Type: enum.ShippingAddress, 41 | }, 42 | } 43 | c = &entity.Cart{ 44 | ID: "123", 45 | UserID: "yauritux", 46 | Items: make([]*vo.CartItem, 0), 47 | CreatedAt: time.Now(), 48 | } 49 | p = &entity.Product{ 50 | ID: "001", 51 | Name: "Shuriken", 52 | Stock: 100, 53 | Price: 125.5, 54 | Disc: 0.0, 55 | } 56 | } 57 | 58 | func TestSpec(t *testing.T) { 59 | 60 | Convey("1. Given a new cart object", t, func() { 61 | setup() 62 | userCart := NewUserCart(u, c) 63 | Convey("-> Cart initial status should equal to Open", func() { 64 | So(userCart.cart.Status, ShouldEqual, enum.Open) 65 | }) 66 | Convey("-> Cart user should not be empty", func() { 67 | So(userCart.user, ShouldNotBeEmpty) 68 | So(userCart.user, ShouldEqual, u) 69 | }) 70 | Convey("-> Cart object should not be empty", func() { 71 | So(userCart.cart, ShouldNotBeEmpty) 72 | So(userCart.cart, ShouldEqual, c) 73 | }) 74 | }) 75 | 76 | Convey("2. Given adding an item to cart", t, func() { 77 | 78 | Convey("-> Negative Scenarios", func() { 79 | setup() 80 | userCart := NewUserCart(u, c) 81 | Convey("-> When item is out of stock", func() { 82 | Convey("-> Should return error", func() { 83 | res, err := userCart.AddItemToCart(p, 105) 84 | So(res, ShouldBeNil) 85 | So(err, ShouldNotBeEmpty) 86 | }) 87 | }) 88 | Convey("-> When cart session id is missing", func() { 89 | Convey("-> Should return error", func() { 90 | c.ID = "" 91 | res, err := userCart.AddItemToCart(p, 10) 92 | So(res, ShouldBeNil) 93 | So(err, ShouldNotBeEmpty) 94 | }) 95 | }) 96 | Convey("-> When user id is missing", func() { 97 | Convey("-> Should return error", func() { 98 | c.UserID = "" 99 | res, err := userCart.AddItemToCart(p, 5) 100 | So(res, ShouldBeNil) 101 | So(err, ShouldNotBeEmpty) 102 | }) 103 | }) 104 | Convey("-> When cart is closed, means all items within cart is settled/paid", func() { 105 | Convey("-> Should return error", func() { 106 | c.Status = enum.Closed 107 | res, err := userCart.AddItemToCart(p, 5) 108 | So(res, ShouldBeNil) 109 | So(err, ShouldNotBeEmpty) 110 | }) 111 | }) 112 | }) 113 | 114 | Convey("-> Positive Scenarios", func() { 115 | setup() 116 | userCart := NewUserCart(u, c) 117 | Convey("-> When new item is added", func() { 118 | Convey("-> requested item should exist within the cart", func() { 119 | addedItem, err := userCart.AddItemToCart(p, 5) 120 | So(err, ShouldBeEmpty) 121 | So(addedItem.ProdID, ShouldEqual, p.ID) 122 | So(addedItem.Qty == 5, ShouldBeTrue) 123 | }) 124 | }) 125 | Convey("-> When the same item already exist inside the cart", func() { 126 | userCart.AddItemToCart(p, 5) 127 | Convey("-> should update the existing cart's item quantity with the one being added", func() { 128 | addedItem, err := userCart.AddItemToCart(p, 2) 129 | So(err, ShouldNotBeEmpty) 130 | So(addedItem.Qty == 7, ShouldBeTrue) 131 | }) 132 | }) 133 | }) 134 | }) 135 | 136 | Convey("3. Given update the cart item", t, func() { 137 | 138 | Convey("-> Negative Scenarios", func() { 139 | setup() 140 | userCart := NewUserCart(u, c) 141 | 142 | Convey("-> When cart is empty", func() { 143 | Convey("-> Should return error with message cart still empty", func() { 144 | err := userCart.UpdateItemInCart( 145 | &vo.CartItem{ 146 | ProdID: "001", 147 | ProdName: "Shuriken", 148 | Qty: 5, 149 | Price: 125.5, 150 | Disc: 0.0, 151 | }, 152 | ) 153 | So(err, ShouldNotBeEmpty) 154 | So(err.Error(), ShouldEqual, "cart is still empty") 155 | }) 156 | }) 157 | 158 | Convey("-> Item does not exist in the cart", func() { 159 | Convey("-> Should return error with message cannot find cart item", func() { 160 | userCart.AddItemToCart(p, 5) 161 | err := userCart.UpdateItemInCart(&vo.CartItem{ 162 | ProdID: "002", 163 | ProdName: "Katana", 164 | Qty: 3, 165 | Price: 750.75, 166 | Disc: 0.0, 167 | }) 168 | So(err, ShouldNotBeEmpty) 169 | So(err.Error(), ShouldEqual, "cannot find cart item 002 - Katana within the cart") 170 | }) 171 | }) 172 | }) 173 | 174 | Convey("-> Positive Scenarios", func() { 175 | setup() 176 | userCart := NewUserCart(u, c) 177 | 178 | Convey("-> No error should be thrown", func() { 179 | userCart.AddItemToCart(p, 5) 180 | err := userCart.UpdateItemInCart(&vo.CartItem{ 181 | ProdID: p.ID, 182 | ProdName: p.Name, 183 | Qty: 3, 184 | Price: p.Price, 185 | Disc: p.Disc, 186 | }) 187 | So(err, ShouldBeEmpty) 188 | }) 189 | }) 190 | }) 191 | 192 | Convey("4. Given remove item from cart", t, func() { 193 | 194 | Convey("-> Negative Scenarios", func() { 195 | setup() 196 | userCart := NewUserCart(u, c) 197 | 198 | Convey("-> When cart is empty", func() { 199 | Convey("-> Should got an error with message cart is still empty", func() { 200 | err := userCart.RemoveItemFromCart("001") 201 | So(err, ShouldNotBeEmpty) 202 | So(err.Error(), ShouldEqual, "cart is still empty") 203 | }) 204 | }) 205 | 206 | Convey("-> When trying to remove an unexisting item from the cart", func() { 207 | Convey("-> Should got an error with message cannot find the cart item", func() { 208 | userCart.AddItemToCart(p, 5) 209 | err := userCart.RemoveItemFromCart("002") 210 | So(err, ShouldNotBeEmpty) 211 | So(err.Error(), ShouldEqual, "cannot find cart item with ID 002") 212 | }) 213 | }) 214 | }) 215 | 216 | Convey("-> Positive Scenarios", func() { 217 | setup() 218 | userCart := NewUserCart(u, c) 219 | 220 | Convey("-> The item should be removed from the cart", func() { 221 | userCart.AddItemToCart(p, 5) 222 | err := userCart.RemoveItemFromCart(p.ID) 223 | So(err, ShouldBeEmpty) 224 | }) 225 | }) 226 | }) 227 | } 228 | --------------------------------------------------------------------------------