From 9c63088ffa88747e95a7254f49d8d00c180e1434 Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Thu, 14 Nov 2024 07:23:11 -0300 Subject: [PATCH 1/2] feat: added new sub command create on command product --- .../mdz/internal/domain/repository/product.go | 7 + .../domain/repository/product_mock.go | 56 +++++++ .../.fixtures/product_response_create.json | 18 ++ components/mdz/internal/rest/product.go | 56 +++++++ components/mdz/internal/rest/product_test.go | 88 ++++++++++ components/mdz/pkg/cmd/product/create.go | 155 ++++++++++++++++++ components/mdz/pkg/cmd/product/create_test.go | 84 ++++++++++ components/mdz/pkg/cmd/product/product.go | 40 +++++ 8 files changed, 504 insertions(+) create mode 100644 components/mdz/internal/domain/repository/product.go create mode 100644 components/mdz/internal/domain/repository/product_mock.go create mode 100644 components/mdz/internal/rest/.fixtures/product_response_create.json create mode 100644 components/mdz/internal/rest/product.go create mode 100644 components/mdz/internal/rest/product_test.go create mode 100644 components/mdz/pkg/cmd/product/create.go create mode 100644 components/mdz/pkg/cmd/product/create_test.go create mode 100644 components/mdz/pkg/cmd/product/product.go diff --git a/components/mdz/internal/domain/repository/product.go b/components/mdz/internal/domain/repository/product.go new file mode 100644 index 00000000..38b6300e --- /dev/null +++ b/components/mdz/internal/domain/repository/product.go @@ -0,0 +1,7 @@ +package repository + +import "github.com/LerianStudio/midaz/common/mmodel" + +type Product interface { + Create(organizationID, ledgerID string, inp mmodel.CreateProductInput) (*mmodel.Product, error) +} diff --git a/components/mdz/internal/domain/repository/product_mock.go b/components/mdz/internal/domain/repository/product_mock.go new file mode 100644 index 00000000..ed69086a --- /dev/null +++ b/components/mdz/internal/domain/repository/product_mock.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: /home/max/Workspace/midaz/components/mdz/internal/domain/repository/product.go +// +// Generated by this command: +// +// mockgen -source=/home/max/Workspace/midaz/components/mdz/internal/domain/repository/product.go -destination=/home/max/Workspace/midaz/components/mdz/internal/domain/repository/product_mock.go -package repository +// + +// Package repository is a generated GoMock package. +package repository + +import ( + reflect "reflect" + + mmodel "github.com/LerianStudio/midaz/common/mmodel" + gomock "go.uber.org/mock/gomock" +) + +// MockProduct is a mock of Product interface. +type MockProduct struct { + ctrl *gomock.Controller + recorder *MockProductMockRecorder + isgomock struct{} +} + +// MockProductMockRecorder is the mock recorder for MockProduct. +type MockProductMockRecorder struct { + mock *MockProduct +} + +// NewMockProduct creates a new mock instance. +func NewMockProduct(ctrl *gomock.Controller) *MockProduct { + mock := &MockProduct{ctrl: ctrl} + mock.recorder = &MockProductMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProduct) EXPECT() *MockProductMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockProduct) Create(organizationID, ledgerID string, inp mmodel.CreateProductInput) (*mmodel.Product, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", organizationID, ledgerID, inp) + ret0, _ := ret[0].(*mmodel.Product) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockProductMockRecorder) Create(organizationID, ledgerID, inp any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProduct)(nil).Create), organizationID, ledgerID, inp) +} diff --git a/components/mdz/internal/rest/.fixtures/product_response_create.json b/components/mdz/internal/rest/.fixtures/product_response_create.json new file mode 100644 index 00000000..866c1b74 --- /dev/null +++ b/components/mdz/internal/rest/.fixtures/product_response_create.json @@ -0,0 +1,18 @@ +{ + "id": "0193271b-877f-7c98-a5a6-43b664d68982", + "name": "Product Refined Cotton Chair", + "ledgerId": "01932715-9f93-7432-90c3-4352bcfe464d", + "organizationId": "01931b04-964a-7caa-a422-c29a95387c00", + "status": { + "code": "ACTIVE", + "description": "Teste Product" + }, + "createdAt": "2024-11-13T19:58:55.868976395Z", + "updatedAt": "2024-11-13T19:58:55.868979258Z", + "deletedAt": null, + "metadata": { + "bitcoin": "3g9ofZcD7KRWL44BWdNa3PyM4PfzgqDG5P", + "chave": "metadata_chave", + "boolean": true + } +} diff --git a/components/mdz/internal/rest/product.go b/components/mdz/internal/rest/product.go new file mode 100644 index 00000000..0ef252ef --- /dev/null +++ b/components/mdz/internal/rest/product.go @@ -0,0 +1,56 @@ +package rest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/LerianStudio/midaz/common/mmodel" + "github.com/LerianStudio/midaz/components/mdz/pkg/factory" +) + +type product struct { + Factory *factory.Factory +} + +func (r *product) Create(organizationID, ledgerID string, inp mmodel.CreateProductInput) (*mmodel.Product, error) { + jsonData, err := json.Marshal(inp) + if err != nil { + return nil, fmt.Errorf("marshalling JSON: %v", err) + } + + uri := fmt.Sprintf("%s/v1/organizations/%s/ledgers/%s/products", + r.Factory.Env.URLAPILedger, organizationID, ledgerID) + + req, err := http.NewRequest(http.MethodPost, uri, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, errors.New("creating request: " + err.Error()) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+r.Factory.Token) + + resp, err := r.Factory.HTTPClient.Do(req) + if err != nil { + return nil, errors.New("making POST request: " + err.Error()) + } + + defer resp.Body.Close() + + if err := checkResponse(resp, http.StatusCreated); err != nil { + return nil, err + } + + var productResp mmodel.Product + if err := json.NewDecoder(resp.Body).Decode(&productResp); err != nil { + return nil, errors.New("decoding response JSON:" + err.Error()) + } + + return &productResp, nil +} + +func NewProduct(f *factory.Factory) *product { + return &product{f} +} diff --git a/components/mdz/internal/rest/product_test.go b/components/mdz/internal/rest/product_test.go new file mode 100644 index 00000000..656183a2 --- /dev/null +++ b/components/mdz/internal/rest/product_test.go @@ -0,0 +1,88 @@ +package rest + +import ( + "fmt" + "net/http" + "testing" + + "github.com/LerianStudio/midaz/common/mmodel" + "github.com/LerianStudio/midaz/components/mdz/pkg/environment" + "github.com/LerianStudio/midaz/components/mdz/pkg/factory" + "github.com/LerianStudio/midaz/components/mdz/pkg/mockutil" + "github.com/LerianStudio/midaz/components/mdz/pkg/ptr" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func Test_product_Create(t *testing.T) { + productID := "0193271b-877f-7c98-a5a6-43b664d68982" + ledgerID := "01932715-9f93-7432-90c3-4352bcfe464d" + organizationID := "01931b04-964a-7caa-a422-c29a95387c00" + + name := "Product Refined Cotton Chair" + code := "ACTIVE" + description := ptr.StringPtr("Teste Product") + + metadata := map[string]any{ + "bitcoin": "3g9ofZcD7KRWL44BWdNa3PyM4PfzgqDG5P", + "chave": "metadata_chave", + "boolean": true, + } + + input := mmodel.CreateProductInput{ + Name: name, + Status: mmodel.Status{ + Code: code, + Description: description, + }, + Metadata: metadata, + } + + expectedResult := &mmodel.Product{ + ID: productID, + Name: name, + LedgerID: ledgerID, + OrganizationID: organizationID, + Status: mmodel.Status{ + Code: code, + Description: description, + }, + Metadata: metadata, + } + + client := &http.Client{} + httpmock.ActivateNonDefault(client) + defer httpmock.DeactivateAndReset() + + URIAPILedger := "http://127.0.0.1:3000" + + uri := fmt.Sprintf("%s/v1/organizations/%s/ledgers/%s/products", + URIAPILedger, organizationID, ledgerID) + + httpmock.RegisterResponder(http.MethodPost, uri, + mockutil.MockResponseFromFile(http.StatusCreated, "./.fixtures/product_response_create.json")) + + factory := &factory.Factory{ + HTTPClient: client, + Env: &environment.Env{ + URLAPILedger: URIAPILedger, + }, + } + + productServ := NewProduct(factory) + + result, err := productServ.Create(organizationID, ledgerID, input) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, expectedResult.ID, result.ID) + assert.Equal(t, expectedResult.Name, result.Name) + assert.Equal(t, expectedResult.OrganizationID, result.OrganizationID) + assert.Equal(t, expectedResult.LedgerID, result.LedgerID) + assert.Equal(t, expectedResult.Status.Code, result.Status.Code) + assert.Equal(t, expectedResult.Status.Description, result.Status.Description) + assert.Equal(t, expectedResult.Metadata, result.Metadata) + + info := httpmock.GetCallCountInfo() + assert.Equal(t, 1, info["POST "+uri]) +} diff --git a/components/mdz/pkg/cmd/product/create.go b/components/mdz/pkg/cmd/product/create.go new file mode 100644 index 00000000..26d971c6 --- /dev/null +++ b/components/mdz/pkg/cmd/product/create.go @@ -0,0 +1,155 @@ +package product + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/LerianStudio/midaz/common/mmodel" + "github.com/LerianStudio/midaz/components/mdz/internal/domain/repository" + "github.com/LerianStudio/midaz/components/mdz/internal/rest" + "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/utils" + "github.com/LerianStudio/midaz/components/mdz/pkg/factory" + "github.com/LerianStudio/midaz/components/mdz/pkg/output" + "github.com/LerianStudio/midaz/components/mdz/pkg/tui" + "github.com/spf13/cobra" +) + +type factoryProductCreate struct { + factory *factory.Factory + repoProduct repository.Product + tuiInput func(message string) (string, error) + flagsCreate +} + +type flagsCreate struct { + OrganizationID string + LedgerID string + Name string + Code string + Description string + Metadata string + JSONFile string +} + +func (f *factoryProductCreate) runE(cmd *cobra.Command, _ []string) error { + product := mmodel.CreateProductInput{} + + if !cmd.Flags().Changed("organization-id") && len(f.OrganizationID) < 1 { + id, err := tui.Input("Enter your organization-id") + if err != nil { + return err + } + + f.OrganizationID = id + } + + if !cmd.Flags().Changed("ledger-id") && len(f.LedgerID) < 1 { + id, err := tui.Input("Enter your ledger-id") + if err != nil { + return err + } + + f.LedgerID = id + } + + if cmd.Flags().Changed("json-file") { + err := utils.FlagFileUnmarshalJSON(f.JSONFile, &product) + if err != nil { + return errors.New("failed to decode the given 'json' file. Verify if " + + "the file format is JSON or fix its content according to the JSON format " + + "specification at https://www.json.org/json-en.html") + } + } else { + err := f.createRequestFromFlags(&product) + if err != nil { + return err + } + } + + resp, err := f.repoProduct.Create(f.OrganizationID, f.LedgerID, product) + if err != nil { + return err + } + + output.Printf(f.factory.IOStreams.Out, + fmt.Sprintf("The Product ID %s has been successfully created", resp.ID)) + + return nil +} + +func (f *factoryProductCreate) createRequestFromFlags(portfolio *mmodel.CreateProductInput) error { + var err error + + portfolio.Name, err = utils.AssignStringField(f.Name, "name", f.tuiInput) + if err != nil { + return err + } + + portfolio.Status.Code = f.Code + + if len(f.Description) > 0 { + portfolio.Status.Description = &f.Description + } + + var metadata map[string]any + if err := json.Unmarshal([]byte(f.Metadata), &metadata); err != nil { + return errors.New("Error parsing metadata: " + err.Error()) + } + + portfolio.Metadata = metadata + + return nil +} + +func (f *factoryProductCreate) setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&f.OrganizationID, + "organization-id", "", "Specify the organization ID.") + cmd.Flags().StringVar(&f.LedgerID, + "ledger-id", "", "Specify the ledger ID.") + cmd.Flags().StringVar(&f.Name, "name", "", + "name new ledger your organization") + cmd.Flags().StringVar(&f.Code, "status-code", "", + "code for the organization (e.g., ACTIVE).") + cmd.Flags().StringVar(&f.Description, "status-description", "", + "Description of the current status of the ledger.") + cmd.Flags().StringVar(&f.Metadata, "metadata", "{}", + "Metadata in JSON format, ex: '{\"key1\": \"value\", \"key2\": 123}'") + cmd.Flags().StringVar(&f.JSONFile, "json-file", "", + `Path to a JSON file containing the attributes of the Product being + created; you can use - for reading from stdin`) + cmd.Flags().BoolP("help", "h", false, + "Displays more information about the Mdz CLI") +} + +func newInjectFacCreate(f *factory.Factory) *factoryProductCreate { + return &factoryProductCreate{ + factory: f, + repoProduct: rest.NewProduct(f), + tuiInput: tui.Input, + } +} + +func newCmdProductCreate(f *factoryProductCreate) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new product for clustering customers.", + Long: utils.Format( + "The create subcommand allows you to set up a new product, defining", + "the policies and grouping rules to organize customers according to", + "specific characteristics. This feature is useful for establishing", + "new clusters and targeting business strategies at specific groups.", + ), + Example: utils.Format( + "$ mdz product create", + "$ mdz product create -h", + "$ mdz product create --json-file payload.json", + "$ cat payload.json | mdz product create --json-file -", + ), + RunE: f.runE, + } + + f.setFlags(cmd) + + return cmd +} diff --git a/components/mdz/pkg/cmd/product/create_test.go b/components/mdz/pkg/cmd/product/create_test.go new file mode 100644 index 00000000..726e7f57 --- /dev/null +++ b/components/mdz/pkg/cmd/product/create_test.go @@ -0,0 +1,84 @@ +package product + +import ( + "bytes" + "testing" + + "github.com/LerianStudio/midaz/common/mmodel" + "github.com/LerianStudio/midaz/components/mdz/internal/domain/repository" + "github.com/LerianStudio/midaz/components/mdz/pkg/factory" + "github.com/LerianStudio/midaz/components/mdz/pkg/iostreams" + "github.com/LerianStudio/midaz/components/mdz/pkg/ptr" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func Test_newCmdProductCreate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockRepo := repository.NewMockProduct(ctrl) + + productID := "01931c99-adef-7b98-ad68-72d7e263066a" + ledgerID := "0192e251-328d-7390-99f5-5c54980115ed" + organizationID := "0192e250-ed9d-7e5c-a614-9b294151b572" + + name := "Romaguera and Sons" + code := "ACTIVE" + description := "Teste Ledger" + + metadata := map[string]any{ + "chave1": "valor1", + "chave2": 2, + "chave3": true, + } + + orgFactory := factoryProductCreate{ + factory: &factory.Factory{IOStreams: &iostreams.IOStreams{ + Out: &bytes.Buffer{}, + Err: &bytes.Buffer{}, + }}, + repoProduct: mockRepo, + tuiInput: func(message string) (string, error) { + return name, nil + }, + flagsCreate: flagsCreate{ + OrganizationID: organizationID, + LedgerID: ledgerID, + Name: name, + Code: code, + Description: description, + Metadata: "{\"chave1\": \"valor1\", \"chave2\": 2, \"chave3\": true}", + }, + } + + cmd := newCmdProductCreate(&orgFactory) + cmd.SetArgs([]string{ + "--organization-id", organizationID, + "--ledger-id", ledgerID, + "--name", name, + "--status-code", code, + "--status-description", description, + "--metadata", "{\"chave1\": \"valor1\", \"chave2\": 2, \"chave3\": true}", + }) + + result := &mmodel.Product{ + ID: productID, + LedgerID: ledgerID, + OrganizationID: organizationID, + Name: name, + Status: mmodel.Status{ + Code: code, + Description: ptr.StringPtr(description), + }, + Metadata: metadata, + } + + mockRepo.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(result, nil) + err := cmd.Execute() + assert.NoError(t, err) + + output := orgFactory.factory.IOStreams.Out.(*bytes.Buffer).String() + assert.Contains(t, output, "The Product ID 01931c99-adef-7b98-ad68-72d7e263066a has been successfully created") + +} diff --git a/components/mdz/pkg/cmd/product/product.go b/components/mdz/pkg/cmd/product/product.go new file mode 100644 index 00000000..d39eb0a8 --- /dev/null +++ b/components/mdz/pkg/cmd/product/product.go @@ -0,0 +1,40 @@ +package product + +import ( + "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/utils" + "github.com/LerianStudio/midaz/components/mdz/pkg/factory" + "github.com/spf13/cobra" +) + +type factoryPortfolio struct { + factory *factory.Factory +} + +func (f *factoryPortfolio) setCmds(cmd *cobra.Command) { + cmd.AddCommand(newCmdProductCreate(newInjectFacCreate(f.factory))) +} + +func NewCmdPortfolio(f *factory.Factory) *cobra.Command { + fOrg := factoryPortfolio{ + factory: f, + } + cmd := &cobra.Command{ + Use: "portfolio", + Short: "Manages the creation and maintenance of products for customer clustering.", + Long: utils.Format( + "The product command allows you to create and manage products, which", + "represent groupings of customers based on specific criteria. This", + "functionality makes it easier to define clustering policies for ", + "better customer management, allowing you to configure, update, view", + "and delete products.", + ), + Example: utils.Format( + "$ mdz product", + "$ mdz product -h", + ), + } + cmd.Flags().BoolP("help", "h", false, "Displays more information about the Midaz CLI") + fOrg.setCmds(cmd) + + return cmd +} From d0c2f898e2ad29fc864eb3545b0cd0eb86caaec3 Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Thu, 14 Nov 2024 07:32:54 -0300 Subject: [PATCH 2/2] feat: added command product in root --- components/mdz/pkg/cmd/product/product.go | 10 +++++----- components/mdz/pkg/cmd/root/root.go | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/mdz/pkg/cmd/product/product.go b/components/mdz/pkg/cmd/product/product.go index d39eb0a8..9acdb7f8 100644 --- a/components/mdz/pkg/cmd/product/product.go +++ b/components/mdz/pkg/cmd/product/product.go @@ -6,20 +6,20 @@ import ( "github.com/spf13/cobra" ) -type factoryPortfolio struct { +type factoryProduct struct { factory *factory.Factory } -func (f *factoryPortfolio) setCmds(cmd *cobra.Command) { +func (f *factoryProduct) setCmds(cmd *cobra.Command) { cmd.AddCommand(newCmdProductCreate(newInjectFacCreate(f.factory))) } -func NewCmdPortfolio(f *factory.Factory) *cobra.Command { - fOrg := factoryPortfolio{ +func NewCmdProduct(f *factory.Factory) *cobra.Command { + fOrg := factoryProduct{ factory: f, } cmd := &cobra.Command{ - Use: "portfolio", + Use: "product", Short: "Manages the creation and maintenance of products for customer clustering.", Long: utils.Format( "The product command allows you to create and manage products, which", diff --git a/components/mdz/pkg/cmd/root/root.go b/components/mdz/pkg/cmd/root/root.go index 73623225..fb421088 100644 --- a/components/mdz/pkg/cmd/root/root.go +++ b/components/mdz/pkg/cmd/root/root.go @@ -8,6 +8,7 @@ import ( "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/login" "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/organization" "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/portfolio" + "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/product" "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/utils" "github.com/LerianStudio/midaz/components/mdz/pkg/cmd/version" "github.com/LerianStudio/midaz/components/mdz/pkg/factory" @@ -23,11 +24,11 @@ type factoryRoot struct { func (f *factoryRoot) setCmds(cmd *cobra.Command) { cmd.AddCommand(version.NewCmdVersion(f.factory)) cmd.AddCommand(login.NewCmdLogin(f.factory)) - cmd.AddCommand(organization.NewCmdOrganization(f.factory)) cmd.AddCommand(ledger.NewCmdLedger(f.factory)) cmd.AddCommand(asset.NewCmdAsset(f.factory)) cmd.AddCommand(portfolio.NewCmdPortfolio(f.factory)) + cmd.AddCommand(product.NewCmdProduct(f.factory)) } func (f *factoryRoot) setFlags(cmd *cobra.Command) {