diff --git a/model/client4.go b/model/client4.go index d6cc62ba0f..74b65948b0 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8803,3 +8803,17 @@ func (c *Client4) GetWorkTemplatesByCategory(category string) ([]*WorkTemplate, err = json.NewDecoder(r.Body).Decode(&templates) return templates, BuildResponse(r), err } + +func (c *Client4) SubmitTrueUpReview(req map[string]any) (*Response, error) { + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, NewAppError("SubmitTrueUpReview", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + r, err := c.DoAPIPostBytes(c.licenseRoute()+"/review", reqBytes) + if err != nil { + return BuildResponse(r), nil + } + defer closeBody(r) + + return BuildResponse(r), nil +} diff --git a/plugin/api_timer_layer_generated.go b/plugin/api_timer_layer_generated.go index a084188c62..c54c6ac7bb 100644 --- a/plugin/api_timer_layer_generated.go +++ b/plugin/api_timer_layer_generated.go @@ -11,8 +11,8 @@ import ( "net/http" timePkg "time" - "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces" ) type apiTimerLayer struct { diff --git a/plugin/hooks_timer_layer_generated.go b/plugin/hooks_timer_layer_generated.go index 6093048d54..87e79ca7e6 100644 --- a/plugin/hooks_timer_layer_generated.go +++ b/plugin/hooks_timer_layer_generated.go @@ -11,8 +11,8 @@ import ( "net/http" timePkg "time" - "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces" ) type hooksTimerLayer struct { diff --git a/server/channels/api4/license.go b/server/channels/api4/license.go index 9911c241e1..ff63704774 100644 --- a/server/channels/api4/license.go +++ b/server/channels/api4/license.go @@ -351,18 +351,21 @@ func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) { return } - // Do not send true-up review data if the user has already requested one for the quarter. - // And only send a true-up review via as a one-time telemetry request if telemetry is disabled. + // True-up is only enabled when telemetry is disabled. + // When telemetry is enabled, we already have all the data necessary for true-up reviews to be completed. telemetryEnabled := c.App.Config().LogSettings.EnableDiagnostics if telemetryEnabled != nil && !*telemetryEnabled { - // Send telemetry data - c.App.Srv().GetTelemetryService().SendTelemetry(model.TrueUpReviewTelemetryName, profileMap) - - // Update the review status to reflect the completion. - status.Completed = true - c.App.Srv().Store().TrueUpReview().Update(status) + err = c.App.Cloud().SubmitTrueUpReview(c.AppContext.Session().UserId, profileMap) + if err != nil { + c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.failed_to_submit", nil, err.Error(), http.StatusInternalServerError) + return + } } + // Update the review status to reflect the completion. + status.Completed = true + c.App.Srv().Store().TrueUpReview().Update(status) + // Encode to string rather than byte[] otherwise json.Marshal will encode it further. encodedData := b64.StdEncoding.EncodeToString(profileMapJson) responseContent := struct { @@ -370,6 +373,7 @@ func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) { }{Content: encodedData} response, _ := json.Marshal(responseContent) + w.WriteHeader(http.StatusOK) w.Write(response) } diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go index 08a9e57305..d673e13543 100644 --- a/server/channels/api4/license_test.go +++ b/server/channels/api4/license_test.go @@ -478,18 +478,33 @@ func TestRequestRenewalLink(t *testing.T) { } func TestRequestTrueUpReview(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - th.App.Srv().SetLicense(model.NewTestLicense()) - t.Run("returns status 200 when telemetry data sent", func(t *testing.T) { - resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "") + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(model.NewTestLicense()) + + th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password) + + cloud := mocks.CloudInterface{} + cloud.Mock.On("SubmitTrueUpReview", mock.Anything, mock.Anything).Return(nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + var reviewProfile map[string]any + resp, err := th.Client.SubmitTrueUpReview(reviewProfile) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("returns 501 when ran by cloud user", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(model.NewTestLicense()) + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "") @@ -500,12 +515,19 @@ func TestRequestTrueUpReview(t *testing.T) { }) t.Run("returns 403 when user does not have permissions", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(model.NewTestLicense()) + resp, err := th.Client.DoAPIPost("/license/review", "") require.Error(t, err) require.Equal(t, http.StatusForbidden, resp.StatusCode) }) t.Run("returns 400 when license is nil", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(nil) resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "") diff --git a/server/channels/einterfaces/cloud.go b/server/channels/einterfaces/cloud.go index 70cdc4676a..fc5446cd34 100644 --- a/server/channels/einterfaces/cloud.go +++ b/server/channels/einterfaces/cloud.go @@ -48,4 +48,7 @@ type CloudInterface interface { SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error + + // Used only for when a customer has telemetry disabled. In this scenario, true up review telemetry will be submitted via CWS. + SubmitTrueUpReview(userID string, trueUpReviewProfile map[string]any) error } diff --git a/server/channels/einterfaces/mocks/CloudInterface.go b/server/channels/einterfaces/mocks/CloudInterface.go index f84300dbec..5800844da0 100644 --- a/server/channels/einterfaces/mocks/CloudInterface.go +++ b/server/channels/einterfaces/mocks/CloudInterface.go @@ -594,6 +594,20 @@ func (_m *CloudInterface) SelfServeDeleteWorkspace(userID string, deletionReques return r0 } +// SubmitTrueUpReview provides a mock function with given fields: userID, trueUpReviewProfile +func (_m *CloudInterface) SubmitTrueUpReview(userID string, trueUpReviewProfile map[string]interface{}) error { + ret := _m.Called(userID, trueUpReviewProfile) + + var r0 error + if rf, ok := ret.Get(0).(func(string, map[string]interface{}) error); ok { + r0 = rf(userID, trueUpReviewProfile) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SubscribeToNewsletter provides a mock function with given fields: userID, req func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error { ret := _m.Called(userID, req) diff --git a/server/i18n/en.json b/server/i18n/en.json index 5f8740a548..598462a448 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2089,6 +2089,10 @@ "id": "api.license.true_up_review.create_error", "translation": "Could not create true up status record" }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Failed to submit true up review profile to CWS." + }, { "id": "api.license.true_up_review.get_status_error", "translation": "Could not get true up status records"