From ba259a2dd9cae4b8baf660579c6b6568ab1fa04e Mon Sep 17 00:00:00 2001 From: RoxyFarhad <40992044+RoxyFarhad@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:58:01 -0500 Subject: [PATCH] App 6822: Adding CLI command to update the billing service (#4612) --- app/app_client.go | 2 ++ app/app_client_test.go | 18 +++++++++++++++ cli/app.go | 18 +++++++++++++++ cli/client.go | 49 ++++++++++++++++++++++++++++++++++++++++ cli/client_test.go | 25 +++++++++++++++++++++ cli/utils.go | 33 +++++++++++++++++++++++++++ cli/utils_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 196 insertions(+) diff --git a/app/app_client.go b/app/app_client.go index bb0fc1788d6..4761e7f1500 100644 --- a/app/app_client.go +++ b/app/app_client.go @@ -60,6 +60,7 @@ type BillingAddress struct { AddressLine2 *string City string State string + Zipcode string } // Location holds the information of a specific location. @@ -1856,6 +1857,7 @@ func billingAddressToProto(addr *BillingAddress) *pb.BillingAddress { AddressLine_2: addr.AddressLine2, City: addr.City, State: addr.State, + Zipcode: addr.Zipcode, } } diff --git a/app/app_client_test.go b/app/app_client_test.go index be5546bac0f..c3375c61fbe 100644 --- a/app/app_client_test.go +++ b/app/app_client_test.go @@ -725,6 +725,24 @@ func TestAppClient(t *testing.T) { test.That(t, resp, test.ShouldEqual, "test-email") }) + t.Run("UpdateBillingServiceConfig", func(t *testing.T) { + grpcClient.UpdateBillingServiceFunc = func(ctx context.Context, + in *pb.UpdateBillingServiceRequest, opts ...grpc.CallOption, + ) (*pb.UpdateBillingServiceResponse, error) { + test.That(t, in.OrgId, test.ShouldEqual, organizationID) + return &pb.UpdateBillingServiceResponse{}, nil + } + + err := client.UpdateBillingService(context.Background(), organizationID, &BillingAddress{ + AddressLine1: "address_line_1", + AddressLine2: nil, + City: "city", + State: "state", + Zipcode: "zip", + }) + test.That(t, err, test.ShouldBeNil) + }) + t.Run("GetOrganizationsWithAccessToLocation", func(t *testing.T) { expectedOrganizationIdentities := []*OrganizationIdentity{&organizationIdentity} grpcClient.GetOrganizationsWithAccessToLocationFunc = func( diff --git a/cli/app.go b/cli/app.go index 85ee4ccc35a..f02cf248fd1 100644 --- a/cli/app.go +++ b/cli/app.go @@ -125,6 +125,7 @@ const ( cpFlagPreserve = "preserve" organizationFlagSupportEmail = "support-email" + organizationBillingAddress = "address" ) var commonFilterFlags = []cli.Flag{ @@ -383,6 +384,23 @@ var app = &cli.App{ }, Action: GetBillingConfigAction, }, + { + Name: "update", + Usage: "update the billing service update for an organization", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagOrgID, + Required: true, + Usage: "the org to update the billing service for", + }, + &cli.StringFlag{ + Name: organizationBillingAddress, + Required: true, + Usage: "the stringified address that follows the pattern: line1, line2 (optional), city, state, zipcode", + }, + }, + Action: UpdateBillingServiceAction, + }, }, }, { diff --git a/cli/client.go b/cli/client.go index 7e68944fd7e..2d9e3e7a7e3 100644 --- a/cli/client.go +++ b/cli/client.go @@ -177,6 +177,55 @@ func (c *viamClient) organizationsSupportEmailGetAction(cCtx *cli.Context, orgID return nil } +// UpdateBillingServiceAction corresponds to `organizations billing-service update`. +func UpdateBillingServiceAction(cCtx *cli.Context) error { + c, err := newViamClient(cCtx) + if err != nil { + return err + } + orgID := cCtx.String(generalFlagOrgID) + if orgID == "" { + return errors.New("cannot update billing service without an organization ID") + } + + address := cCtx.String(organizationBillingAddress) + if address == "" { + return errors.New("cannot update billing service to an empty address") + } + + return c.updateBillingServiceAction(cCtx, orgID, address) +} + +func (c *viamClient) updateBillingServiceAction(cCtx *cli.Context, orgID, addressAsString string) error { + if err := c.ensureLoggedIn(); err != nil { + return err + } + address, err := parseBillingAddress(addressAsString) + if err != nil { + return err + } + + _, err = c.client.UpdateBillingService(cCtx.Context, &apppb.UpdateBillingServiceRequest{ + OrgId: orgID, + BillingAddress: address, + }) + if err != nil { + return err + } + + printf(cCtx.App.Writer, "Successfully updated billing service for organization %q", orgID) + printf(cCtx.App.Writer, " --- Billing Address --- ") + printf(cCtx.App.Writer, "Address Line 1: %s", address.GetAddressLine_1()) + if address.GetAddressLine_2() != "" { + printf(cCtx.App.Writer, "Address Line 2: %s", address.GetAddressLine_2()) + } + printf(cCtx.App.Writer, "City: %s", address.GetCity()) + printf(cCtx.App.Writer, "State: %s", address.GetState()) + printf(cCtx.App.Writer, "Postal Code: %s", address.GetZipcode()) + printf(cCtx.App.Writer, "Country: USA") + return nil +} + // GetBillingConfigAction corresponds to `organizations billing get`. func GetBillingConfigAction(cCtx *cli.Context) error { c, err := newViamClient(cCtx) diff --git a/cli/client_test.go b/cli/client_test.go index 14c9a37c2ff..9e2f9ce5a33 100644 --- a/cli/client_test.go +++ b/cli/client_test.go @@ -278,6 +278,31 @@ func TestGetBillingConfigAction(t *testing.T) { test.That(t, out.messages[11], test.ShouldContainSubstring, "USA") } +func TestUpdateBillingServiceAction(t *testing.T) { + updateConfigFunc := func(ctx context.Context, in *apppb.UpdateBillingServiceRequest, opts ...grpc.CallOption) ( + *apppb.UpdateBillingServiceResponse, error, + ) { + return &apppb.UpdateBillingServiceResponse{}, nil + } + asc := &inject.AppServiceClient{ + UpdateBillingServiceFunc: updateConfigFunc, + } + + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + address := "123 Main St, Suite 100, San Francisco, CA, 94105" + test.That(t, ac.updateBillingServiceAction(cCtx, "test-org", address), test.ShouldBeNil) + test.That(t, len(errOut.messages), test.ShouldEqual, 0) + test.That(t, len(out.messages), test.ShouldEqual, 8) + test.That(t, out.messages[0], test.ShouldContainSubstring, "Successfully updated billing service for organization") + test.That(t, out.messages[1], test.ShouldContainSubstring, " --- Billing Address --- ") + test.That(t, out.messages[2], test.ShouldContainSubstring, "123 Main St") + test.That(t, out.messages[3], test.ShouldContainSubstring, "Suite 100") + test.That(t, out.messages[4], test.ShouldContainSubstring, "San Francisco") + test.That(t, out.messages[5], test.ShouldContainSubstring, "CA") + test.That(t, out.messages[6], test.ShouldContainSubstring, "94105") + test.That(t, out.messages[7], test.ShouldContainSubstring, "USA") +} + func TestTabularDataByFilterAction(t *testing.T) { pbStruct, err := protoutils.StructToStructPb(map[string]interface{}{"bool": true, "string": "true", "float": float64(1)}) test.That(t, err, test.ShouldBeNil) diff --git a/cli/utils.go b/cli/utils.go index d9653dc7777..94d16624634 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -4,6 +4,10 @@ import ( "fmt" "path/filepath" "regexp" + "strings" + + "github.com/pkg/errors" + apppb "go.viam.com/api/app/v1" ) // samePath returns true if abs(path1) and abs(path2) are the same. @@ -59,3 +63,32 @@ func ParseFileType(raw string) string { } return fmt.Sprintf("%s/%s", osLookup[rawOs], archLookup[rawArch]) } + +func parseBillingAddress(address string) (*apppb.BillingAddress, error) { + if address == "" { + return nil, errors.New("address is empty") + } + + splitAddress := strings.Split(address, ",") + if len(splitAddress) != 4 && len(splitAddress) != 5 { + return nil, errors.Errorf("address: %s does not follow the format: line1, line2 (optional), city, state, zipcode", address) + } + + if len(splitAddress) == 4 { + return &apppb.BillingAddress{ + AddressLine_1: strings.Trim(splitAddress[0], " "), + City: strings.Trim(splitAddress[1], " "), + State: strings.Trim(splitAddress[2], " "), + Zipcode: strings.Trim(splitAddress[3], " "), + }, nil + } + + line2 := strings.Trim(splitAddress[1], " ") + return &apppb.BillingAddress{ + AddressLine_1: strings.Trim(splitAddress[0], " "), + AddressLine_2: &line2, + City: strings.Trim(splitAddress[2], " "), + State: strings.Trim(splitAddress[3], " "), + Zipcode: strings.Trim(splitAddress[4], " "), + }, nil +} diff --git a/cli/utils_test.go b/cli/utils_test.go index e2c26acf567..270dae823fa 100644 --- a/cli/utils_test.go +++ b/cli/utils_test.go @@ -4,6 +4,8 @@ package cli import ( "testing" + "github.com/pkg/errors" + apppb "go.viam.com/api/app/v1" "go.viam.com/test" ) @@ -36,3 +38,52 @@ func TestParseFileType(t *testing.T) { test.That(t, ParseFileType(pair[1]), test.ShouldResemble, pair[0]) } } + +func TestParseBillingAddress(t *testing.T) { + addressLine2 := "Apt 1" + + testCases := []struct { + input string + expectedAddress *apppb.BillingAddress + expectedErr error + }{ + { + input: "123 Main St, Apt 1, San Francisco, CA, 94105", + expectedAddress: &apppb.BillingAddress{ + AddressLine_1: "123 Main St", + AddressLine_2: &addressLine2, + City: "San Francisco", + State: "CA", + Zipcode: "94105", + }, + }, + { + input: "123 Main St, San Francisco, CA, 94105", + expectedAddress: &apppb.BillingAddress{ + AddressLine_1: "123 Main St", + City: "San Francisco", + State: "CA", + Zipcode: "94105", + }, + }, + { + input: "an-invalid address, city-1", + expectedAddress: nil, + expectedErr: errors.New("address: an-invalid address, city-1 does not follow the format: line1, line2 (optional), city, state, zipcode"), + }, + { + input: "", + expectedAddress: nil, + expectedErr: errors.New("address is empty"), + }, + } + + for _, tc := range testCases { + address, err := parseBillingAddress(tc.input) + if tc.expectedErr != nil { + test.That(t, err.Error(), test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, tc.expectedErr.Error()) + } + test.That(t, address, test.ShouldResembleProto, tc.expectedAddress) + } +}