From 3d4e8b16d7e4c6f00b027e65a178508bf7a2bfd8 Mon Sep 17 00:00:00 2001 From: Tom Brooks <100007843+OddTomBrooks@users.noreply.github.com> Date: Tue, 16 Jul 2024 05:47:21 -0400 Subject: [PATCH] [EASI-4473] Notification: Data Exchange Approach Completed (#1209) * feat: added feature migration, data models, gql * chore: updated sql queries * feat: several major improvements to first semi-functional data exchange approach in-app notification * chore: updated postman collection to get all notifications * chore: updated frontend gql and backend gql for user notification preferences * chore: added simple resolver helper to dispatch emails and email test to validate email templates * chore: removed resolved question * chore: Reset all frontend files to main branch as we split the frontend work to EASI-4491 * chore: reran gql gen * chore: email unit test for data exchange approach completed * chore: removed unnecessary Scan and Value methods * chore: added unit test for activity data exchange complete notification * chore: implemented resolver for data exchange approach completed notification preferences * fix: updated various test definitions to match new user account preferences spec * chore: removed outdated comment * chore: updated postman collection renaming markedCompletedBy -> markedCompleteBy * chore: converted data exchange approach complete meta to id --- MINT.postman_collection.json | 4 +- cmd/dbseed/main.go | 37 ++ cmd/test_email/main.go | 29 +- ...change_Approach_Completed_Notification.sql | 7 + pkg/email/data_exchange_approach_completed.go | 15 + pkg/email/template_service_impl.go | 16 + ...data_exchange_approach_completed_body.html | 19 + ...a_exchange_approach_completed_subject.html | 1 + pkg/graph/generated/generated.go | 530 +++++++++++++++++- pkg/graph/resolvers/activity.resolvers.go | 11 + .../added_as_collaborator_email_test.go | 2 +- ..._exchange_approach_completed_email_test.go | 65 +++ .../data_exchange_approach_helper.go | 159 ++++++ pkg/graph/resolvers/plan_collaborator_test.go | 4 +- pkg/graph/resolvers/resolver_test.go | 2 +- .../resolvers/resolver_test_utilities.go | 2 +- ...user_notification_preferences.resolvers.go | 5 + pkg/graph/schema/types/activity.graphql | 11 +- .../user_notification_preferences.graphql | 4 + pkg/models/activity.go | 17 +- pkg/models/data_exchange_approach.go | 24 + ...change_approach_completed_activity_meta.go | 56 ++ pkg/models/user_notification_preferences.go | 36 +- pkg/notifications/activity.go | 9 + .../data_exchange_approach_completed.go | 41 ++ .../data_exchange_approach_completed_test.go | 62 ++ .../user_notification_preferences/create.sql | 3 + .../get_by_user_id.sql | 1 + .../get_by_user_id_loader.sql | 1 + .../user_notification_preferences/update.sql | 2 + src/gql/gen/graphql.ts | 16 +- 31 files changed, 1134 insertions(+), 57 deletions(-) create mode 100644 migrations/V162__Add_Data_Exchange_Approach_Completed_Notification.sql create mode 100644 pkg/email/data_exchange_approach_completed.go create mode 100644 pkg/email/templates/data_exchange_approach_completed_body.html create mode 100644 pkg/email/templates/data_exchange_approach_completed_subject.html create mode 100644 pkg/graph/resolvers/data_exchange_approach_completed_email_test.go create mode 100644 pkg/graph/resolvers/data_exchange_approach_helper.go create mode 100644 pkg/models/data_exchange_approach.go create mode 100644 pkg/models/data_exchange_approach_completed_activity_meta.go create mode 100644 pkg/notifications/data_exchange_approach_completed.go create mode 100644 pkg/notifications/data_exchange_approach_completed_test.go diff --git a/MINT.postman_collection.json b/MINT.postman_collection.json index c05665056f..e02fa2694a 100644 --- a/MINT.postman_collection.json +++ b/MINT.postman_collection.json @@ -2154,7 +2154,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "query currentUser {\n currentUser {\n notifications {\n numUnreadNotifications\n notifications {\n __typename\n id\n isRead\n inAppSent\n emailSent\n activity {\n activityType\n entityID\n actorID\n metaData {\n __typename\n ... on TaggedInPlanDiscussionActivityMeta{\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n content\n }\n ... on TaggedInDiscussionReplyActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n replyID\n content\n }\n ... on ModelPlanSharedActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n }\n optionalMessage\n }\n ... on NewDiscussionRepliedActivityMeta {\n version\n type\n discussionID\n replyID\n content\n } \n ... on DailyDigestCompleteActivityMeta{\n version\n type\n modelPlanIDs\n date\n analyzedAudits{\n id\n modelPlanID\n modelName\n date\n changes{\n modelPlan{\n oldName\n statusChanges\n }\n documents{\n count\n }\n crTdls{\n activity\n }\n planSections{\n updated\n readyForReview\n readyForClearance\n }\n modelLeads{\n added{\n id\n commonName\n # userAccount{\n # id\n # email\n # }\n }\n }\n planDiscussions{\n activity\n }\n }\n\n }\n\n }\n ... on NewModelPlanActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n }\n }\n ... on DatesChangedActivityMeta {\n version\n type\n modelPlanID\n modelPlan {\n id\n }\n dateChanges {\n isChanged\n field\n isRange\n oldDate\n newDate\n oldRangeStart\n oldRangeEnd\n newRangeStart\n newRangeEnd\n }\n } \n }\n createdByUserAccount {\n commonName\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n }\n }\n}\n", + "query": "query currentUser {\n currentUser {\n notifications {\n numUnreadNotifications\n notifications {\n __typename\n id\n isRead\n inAppSent\n emailSent\n activity {\n activityType\n entityID\n actorID\n metaData {\n __typename\n ... on TaggedInPlanDiscussionActivityMeta{\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n content\n }\n ... on TaggedInDiscussionReplyActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n replyID\n content\n }\n ... on ModelPlanSharedActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n }\n optionalMessage\n }\n ... on NewDiscussionRepliedActivityMeta {\n version\n type\n discussionID\n replyID\n content\n } \n ... on DailyDigestCompleteActivityMeta{\n version\n type\n modelPlanIDs\n date\n analyzedAudits{\n id\n modelPlanID\n modelName\n date\n changes{\n modelPlan{\n oldName\n statusChanges\n }\n documents{\n count\n }\n crTdls{\n activity\n }\n planSections{\n updated\n readyForReview\n readyForClearance\n }\n modelLeads{\n added{\n id\n commonName\n # userAccount{\n # id\n # email\n # }\n }\n }\n planDiscussions{\n activity\n }\n }\n\n }\n\n }\n ... on NewModelPlanActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n }\n }\n ... on DatesChangedActivityMeta {\n version\n type\n modelPlanID\n modelPlan {\n id\n }\n dateChanges {\n isChanged\n field\n isRange\n oldDate\n newDate\n oldRangeStart\n oldRangeEnd\n newRangeStart\n newRangeEnd\n }\n }\n ... on DataExchangeApproachCompletedActivityMeta {\n version\n type\n dataExchangeApproach {\n id\n name\n }\n markedCompleteBy\n } \n }\n createdByUserAccount {\n commonName\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n }\n }\n}\n", "variables": "" } }, @@ -2194,7 +2194,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "query currentUser {\n currentUser {\n notificationPreferences {\n dailyDigestComplete\n\n addedAsCollaborator\n\n taggedInDiscussion\n\n taggedInDiscussionReply\n\n newDiscussionReply\n\n modelPlanShared\n\n newModelPlan\n\n datesChanged\n datesChangedNotificationType\n }\n }\n }\n\n", + "query": "query currentUser {\n currentUser {\n notificationPreferences {\n dailyDigestComplete\n\n addedAsCollaborator\n\n taggedInDiscussion\n\n taggedInDiscussionReply\n\n newDiscussionReply\n\n modelPlanShared\n\n newModelPlan\n\n datesChanged\n datesChangedNotificationType\n\n dataExchangeApproachCompleted\n }\n }\n }\n\n", "variables": "" } }, diff --git a/cmd/dbseed/main.go b/cmd/dbseed/main.go index 3df21fe161..7355a1d27f 100644 --- a/cmd/dbseed/main.go +++ b/cmd/dbseed/main.go @@ -258,6 +258,43 @@ func (s *Seeder) SeedData() { }, }, ) + + // Send a notification for Data Exchange Approach Completed + dataExchangeApproach := models.NewDataExchangeApproach( + "Data Exchange Approach", + planWithDocuments.CreatedBy, + planWithDocuments.ID, + ) + + dataExchangeApproach.ID = uuid.MustParse("01020304-0506-0708-090a-0b0c0d0e0f10") + + // create an actor principal for testing notifications + + actorPrincipal := s.getTestPrincipalByUsername("MINT") + + // Use a test user to mark the data exchange approach as complete + testUser := s.getTestPrincipalByUsername("BTAL") + + err = resolvers.SendDataExchangeApproachCompletedNotification( + s.Config.Context, + s.Config.EmailService, + s.Config.EmailTemplateService, + s.Config.AddressBook, + actorPrincipal.UserAccount.ID, + s.Config.Store, + []*models.UserAccountAndNotificationPreferences{ + { + UserAccount: *testUser.UserAccount, + PreferenceFlags: models.DefaultUserNotificationPreferencesFlags(), + }, + }, + planWithDocuments, + dataExchangeApproach, + testUser.UserAccount.ID, + ) + if err != nil { + panic(fmt.Errorf("failed to send data exchange approach completed notification: %w", err)) + } } // CreateAnalyzedAuditData uses the seeder to generate analyzed audits. It will make one record for all changes just seeded diff --git a/cmd/test_email/main.go b/cmd/test_email/main.go index 4c6b8715f7..ed13a424e7 100644 --- a/cmd/test_email/main.go +++ b/cmd/test_email/main.go @@ -21,22 +21,25 @@ func main() { // Running all test functions sendModelPlanCreatedEmailTest(emailService, templateService) + // Discussion emails sendPlanDiscussionCreatedTestEmail(emailService, templateService, addressBook) sendPlanDiscussionTaggedUserTestEmail(emailService, templateService, addressBook) sendPlanDiscussionTaggedSolutionTestEmail(emailService, templateService, addressBook) + //DiscussionReply email sendDiscussionReplyOriginatorTestEmail(emailService, templateService, addressBook) + // Model plan emails sendModelPlanShareTest(emailService, templateService, addressBook) sendDateChangedEmailsTest(emailService, templateService, addressBook) sendCollaboratorAddedEmailTest(emailService, templateService, addressBook) + sendDataExchangeApproachCompletedEmailNotificationTest(emailService, templateService, addressBook) sendFeedbackEmail(emailService, templateService, addressBook) reportAProblemEmail(emailService, templateService, addressBook) // Solution emails sendSolutionSelectedTestEmail(emailService, templateService, addressBook) - } func noErr(err error) { @@ -390,6 +393,30 @@ func sendCollaboratorAddedEmailTest( noErr(err) } +func sendDataExchangeApproachCompletedEmailNotificationTest( + emailService oddmail.EmailService, + templateService email.TemplateService, + addressBook email.AddressBook, +) { + + modelPlan := models.NewModelPlan( + uuid.Nil, + "Retcon Plan", + ) + + err := resolvers.SendDataExchangeApproachCompletedEmailNotification( + emailService, + templateService, + addressBook, + modelPlan, + "marty.mcfly@delorean.88", + "Doc Brown", + true, + ) + + noErr(err) +} + func sendFeedbackEmail( emailService oddmail.EmailService, templateService email.TemplateService, diff --git a/migrations/V162__Add_Data_Exchange_Approach_Completed_Notification.sql b/migrations/V162__Add_Data_Exchange_Approach_Completed_Notification.sql new file mode 100644 index 0000000000..3856bbd120 --- /dev/null +++ b/migrations/V162__Add_Data_Exchange_Approach_Completed_Notification.sql @@ -0,0 +1,7 @@ +ALTER TYPE ACTIVITY_TYPE + ADD VALUE 'DATA_EXCHANGE_APPROACH_COMPLETED' AFTER 'NEW_MODEL_PLAN'; + +ALTER TABLE user_notification_preferences + ADD COLUMN data_exchange_approach_completed user_notification_preference_flag[] DEFAULT '{}'::user_notification_preference_flag[]; + +COMMENT ON COLUMN user_notification_preferences.data_exchange_approach_completed IS 'Notification preference for when a data exchange approach is completed'; diff --git a/pkg/email/data_exchange_approach_completed.go b/pkg/email/data_exchange_approach_completed.go new file mode 100644 index 0000000000..49e9bcf398 --- /dev/null +++ b/pkg/email/data_exchange_approach_completed.go @@ -0,0 +1,15 @@ +package email + +// DataExchangeApproachCompletedSubjectContent defines the parameters necessary for the corresponding email subject +type DataExchangeApproachCompletedSubjectContent struct { + ModelName string +} + +// DataExchangeApproachCompletedBodyContent defines the parameters necessary for the corresponding email body +type DataExchangeApproachCompletedBodyContent struct { + ClientAddress string + ModelName string + ModelID string + MarkedCompletedByUserCommonName string + ShowFooter bool +} diff --git a/pkg/email/template_service_impl.go b/pkg/email/template_service_impl.go index 75e451fd5c..0b4d0c1eab 100644 --- a/pkg/email/template_service_impl.go +++ b/pkg/email/template_service_impl.go @@ -139,6 +139,15 @@ var solutionSelectedBodyTemplate string //go:embed templates/solution_selected_subject.html var solutionSelectedSubjectTemplate string +// DataExchangeApproachCompletedTemplateName is the template name for the data exchange approach completed email +const DataExchangeApproachCompletedTemplateName string = "data_exchange_approach_completed" + +//go:embed templates/data_exchange_approach_completed_body.html +var dataExchangeApproachCompletedBodyTemplate string + +//go:embed templates/data_exchange_approach_completed_subject.html +var dataExchangeApproachCompletedSubjectTemplate string + // TemplateServiceImpl is an implementation-specific structure loading all resources necessary for server execution type TemplateServiceImpl struct { templateCache *emailTemplates.TemplateCache @@ -185,6 +194,7 @@ func (t *TemplateServiceImpl) Load() error { if err != nil { return err } + err = t.loadEmailTemplate(DiscussionReplyCreatedOriginatorTemplateName, discussionReplyCreatedOriginatorSubjectTemplate, discussionReplyCreatedOriginatorBodyTemplate) if err != nil { return err @@ -224,6 +234,12 @@ func (t *TemplateServiceImpl) Load() error { if err != nil { return err } + + err = t.loadEmailTemplate(DataExchangeApproachCompletedTemplateName, dataExchangeApproachCompletedSubjectTemplate, dataExchangeApproachCompletedBodyTemplate) + if err != nil { + return err + } + return nil } diff --git a/pkg/email/templates/data_exchange_approach_completed_body.html b/pkg/email/templates/data_exchange_approach_completed_body.html new file mode 100644 index 0000000000..31070e32f7 --- /dev/null +++ b/pkg/email/templates/data_exchange_approach_completed_body.html @@ -0,0 +1,19 @@ +{{template "shared_header.html" "Data exchange approach complete:"}} + +

{{.ModelName}}

+

Marked complete by {{.MarkedCompletedByUserCommonName}}

+
+

+ View data exchange approach +

+
+ +{{if .ShowFooter}} +
+
+

+ You are currently subscribed to notifications when data exchange approaches are completed. If you no longer wish to receive these email notifications, please unsubscribe or change your notification settings. +

+{{end}} + +{{template "shared_footer.html"}} \ No newline at end of file diff --git a/pkg/email/templates/data_exchange_approach_completed_subject.html b/pkg/email/templates/data_exchange_approach_completed_subject.html new file mode 100644 index 0000000000..965e963b9c --- /dev/null +++ b/pkg/email/templates/data_exchange_approach_completed_subject.html @@ -0,0 +1 @@ +Data exchange approach complete: {{.ModelName}} \ No newline at end of file diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index a57962aae5..66596e4a2e 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -47,6 +47,7 @@ type ResolverRoot interface { AuditChange() AuditChangeResolver CurrentUser() CurrentUserResolver DailyDigestCompleteActivityMeta() DailyDigestCompleteActivityMetaResolver + DataExchangeApproachCompletedActivityMeta() DataExchangeApproachCompletedActivityMetaResolver DatesChangedActivityMeta() DatesChangedActivityMetaResolver DiscussionReply() DiscussionReplyResolver ExistingModelLink() ExistingModelLinkResolver @@ -195,6 +196,14 @@ type ComplexityRoot struct { Version func(childComplexity int) int } + DataExchangeApproachCompletedActivityMeta struct { + DataExchangeApproachID func(childComplexity int) int + MarkedCompleteBy func(childComplexity int) int + MarkedCompleteByUserAccount func(childComplexity int) int + Type func(childComplexity int) int + Version func(childComplexity int) int + } + DateChange struct { Field func(childComplexity int) int IsChanged func(childComplexity int) int @@ -1215,23 +1224,24 @@ type ComplexityRoot struct { } UserNotificationPreferences struct { - AddedAsCollaborator func(childComplexity int) int - CreatedBy func(childComplexity int) int - CreatedByUserAccount func(childComplexity int) int - CreatedDts func(childComplexity int) int - DailyDigestComplete func(childComplexity int) int - DatesChanged func(childComplexity int) int - DatesChangedNotificationType func(childComplexity int) int - ID func(childComplexity int) int - ModelPlanShared func(childComplexity int) int - ModifiedBy func(childComplexity int) int - ModifiedByUserAccount func(childComplexity int) int - ModifiedDts func(childComplexity int) int - NewDiscussionReply func(childComplexity int) int - NewModelPlan func(childComplexity int) int - TaggedInDiscussion func(childComplexity int) int - TaggedInDiscussionReply func(childComplexity int) int - UserID func(childComplexity int) int + AddedAsCollaborator func(childComplexity int) int + CreatedBy func(childComplexity int) int + CreatedByUserAccount func(childComplexity int) int + CreatedDts func(childComplexity int) int + DailyDigestComplete func(childComplexity int) int + DataExchangeApproachCompleted func(childComplexity int) int + DatesChanged func(childComplexity int) int + DatesChangedNotificationType func(childComplexity int) int + ID func(childComplexity int) int + ModelPlanShared func(childComplexity int) int + ModifiedBy func(childComplexity int) int + ModifiedByUserAccount func(childComplexity int) int + ModifiedDts func(childComplexity int) int + NewDiscussionReply func(childComplexity int) int + NewModelPlan func(childComplexity int) int + TaggedInDiscussion func(childComplexity int) int + TaggedInDiscussionReply func(childComplexity int) int + UserID func(childComplexity int) int } UserNotifications struct { @@ -1278,6 +1288,9 @@ type CurrentUserResolver interface { type DailyDigestCompleteActivityMetaResolver interface { AnalyzedAudits(ctx context.Context, obj *models.DailyDigestCompleteActivityMeta) ([]*models.AnalyzedAudit, error) } +type DataExchangeApproachCompletedActivityMetaResolver interface { + MarkedCompleteByUserAccount(ctx context.Context, obj *models.DataExchangeApproachCompletedActivityMeta) (*authentication.UserAccount, error) +} type DatesChangedActivityMetaResolver interface { ModelPlan(ctx context.Context, obj *models.DatesChangedActivityMeta) (*models.ModelPlan, error) } @@ -1582,6 +1595,8 @@ type UserNotificationPreferencesResolver interface { ModelPlanShared(ctx context.Context, obj *models.UserNotificationPreferences) ([]models.UserNotificationPreferenceFlag, error) NewModelPlan(ctx context.Context, obj *models.UserNotificationPreferences) ([]models.UserNotificationPreferenceFlag, error) DatesChanged(ctx context.Context, obj *models.UserNotificationPreferences) ([]models.UserNotificationPreferenceFlag, error) + + DataExchangeApproachCompleted(ctx context.Context, obj *models.UserNotificationPreferences) ([]models.UserNotificationPreferenceFlag, error) } type UserViewCustomizationResolver interface { ViewCustomization(ctx context.Context, obj *models.UserViewCustomization) ([]models.ViewCustomizationType, error) @@ -2065,6 +2080,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DailyDigestCompleteActivityMeta.Version(childComplexity), true + case "DataExchangeApproachCompletedActivityMeta.dataExchangeApproachID": + if e.complexity.DataExchangeApproachCompletedActivityMeta.DataExchangeApproachID == nil { + break + } + + return e.complexity.DataExchangeApproachCompletedActivityMeta.DataExchangeApproachID(childComplexity), true + + case "DataExchangeApproachCompletedActivityMeta.markedCompleteBy": + if e.complexity.DataExchangeApproachCompletedActivityMeta.MarkedCompleteBy == nil { + break + } + + return e.complexity.DataExchangeApproachCompletedActivityMeta.MarkedCompleteBy(childComplexity), true + + case "DataExchangeApproachCompletedActivityMeta.markedCompleteByUserAccount": + if e.complexity.DataExchangeApproachCompletedActivityMeta.MarkedCompleteByUserAccount == nil { + break + } + + return e.complexity.DataExchangeApproachCompletedActivityMeta.MarkedCompleteByUserAccount(childComplexity), true + + case "DataExchangeApproachCompletedActivityMeta.type": + if e.complexity.DataExchangeApproachCompletedActivityMeta.Type == nil { + break + } + + return e.complexity.DataExchangeApproachCompletedActivityMeta.Type(childComplexity), true + + case "DataExchangeApproachCompletedActivityMeta.version": + if e.complexity.DataExchangeApproachCompletedActivityMeta.Version == nil { + break + } + + return e.complexity.DataExchangeApproachCompletedActivityMeta.Version(childComplexity), true + case "DateChange.field": if e.complexity.DateChange.Field == nil { break @@ -8592,6 +8642,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.UserNotificationPreferences.DailyDigestComplete(childComplexity), true + case "UserNotificationPreferences.dataExchangeApproachCompleted": + if e.complexity.UserNotificationPreferences.DataExchangeApproachCompleted == nil { + break + } + + return e.complexity.UserNotificationPreferences.DataExchangeApproachCompleted(childComplexity), true + case "UserNotificationPreferences.datesChanged": if e.complexity.UserNotificationPreferences.DatesChanged == nil { break @@ -8928,6 +8985,7 @@ enum ActivityType { MODEL_PLAN_SHARED NEW_MODEL_PLAN DATES_CHANGED + DATA_EXCHANGE_APPROACH_COMPLETED } enum DateChangeFieldType { @@ -8942,7 +9000,7 @@ enum DateChangeFieldType { """ ActivityMetaData is a type that represents all the data that can be captured in an Activity """ -union ActivityMetaData = TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | DailyDigestCompleteActivityMeta | NewDiscussionRepliedActivityMeta | AddedAsCollaboratorMeta | ModelPlanSharedActivityMeta | NewModelPlanActivityMeta | DatesChangedActivityMeta +union ActivityMetaData = TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | DailyDigestCompleteActivityMeta | NewDiscussionRepliedActivityMeta | AddedAsCollaboratorMeta | ModelPlanSharedActivityMeta | NewModelPlanActivityMeta | DatesChangedActivityMeta | DataExchangeApproachCompletedActivityMeta type AddedAsCollaboratorMeta { version: Int! @@ -9031,6 +9089,14 @@ type DatesChangedActivityMeta { dateChanges: [DateChange!]! } +type DataExchangeApproachCompletedActivityMeta { + version: Int! + type: ActivityType! + dataExchangeApproachID: UUID! + markedCompleteBy: UUID! + markedCompleteByUserAccount: UserAccount! +} + """ Activity represents an event that happened in the application that could result in a notification. """ @@ -9523,9 +9589,6 @@ extend type ModelPlan { } type ModelPlanAndOperationalSolution { - # TODO -- we can uncomment these if we want? - # modelPlanID: UUID! - # operationalSolutionID: UUID! operationalSolution: OperationalSolution! modelPlan: ModelPlan! } @@ -11999,6 +12062,8 @@ type UserNotificationPreferences { datesChanged: [UserNotificationPreferenceFlag!]! datesChangedNotificationType: DatesChangedNotificationType + dataExchangeApproachCompleted: [UserNotificationPreferenceFlag!]! + createdBy: UUID! createdByUserAccount: UserAccount! createdDts: Time! @@ -12029,6 +12094,8 @@ input UserNotificationPreferencesChanges @goModel(model: "map[string]interface{} datesChanged: [UserNotificationPreferenceFlag!] datesChangedNotificationType: DatesChangedNotificationType + + dataExchangeApproachCompleted: [UserNotificationPreferenceFlag!] } extend type Mutation { @@ -16329,6 +16396,8 @@ func (ec *executionContext) fieldContext_CurrentUser_notificationPreferences(ctx return ec.fieldContext_UserNotificationPreferences_datesChanged(ctx, field) case "datesChangedNotificationType": return ec.fieldContext_UserNotificationPreferences_datesChangedNotificationType(ctx, field) + case "dataExchangeApproachCompleted": + return ec.fieldContext_UserNotificationPreferences_dataExchangeApproachCompleted(ctx, field) case "createdBy": return ec.fieldContext_UserNotificationPreferences_createdBy(ctx, field) case "createdByUserAccount": @@ -16636,6 +16705,248 @@ func (ec *executionContext) fieldContext_DailyDigestCompleteActivityMeta_date(ct return fc, nil } +func (ec *executionContext) _DataExchangeApproachCompletedActivityMeta_version(ctx context.Context, field graphql.CollectedField, obj *models.DataExchangeApproachCompletedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DataExchangeApproachCompletedActivityMeta_version(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Version, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DataExchangeApproachCompletedActivityMeta_version(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataExchangeApproachCompletedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DataExchangeApproachCompletedActivityMeta_type(ctx context.Context, field graphql.CollectedField, obj *models.DataExchangeApproachCompletedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DataExchangeApproachCompletedActivityMeta_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(models.ActivityType) + fc.Result = res + return ec.marshalNActivityType2githubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐActivityType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DataExchangeApproachCompletedActivityMeta_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataExchangeApproachCompletedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ActivityType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DataExchangeApproachCompletedActivityMeta_dataExchangeApproachID(ctx context.Context, field graphql.CollectedField, obj *models.DataExchangeApproachCompletedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DataExchangeApproachCompletedActivityMeta_dataExchangeApproachID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DataExchangeApproachID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DataExchangeApproachCompletedActivityMeta_dataExchangeApproachID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataExchangeApproachCompletedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DataExchangeApproachCompletedActivityMeta_markedCompleteBy(ctx context.Context, field graphql.CollectedField, obj *models.DataExchangeApproachCompletedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DataExchangeApproachCompletedActivityMeta_markedCompleteBy(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.MarkedCompleteBy, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DataExchangeApproachCompletedActivityMeta_markedCompleteBy(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataExchangeApproachCompletedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DataExchangeApproachCompletedActivityMeta_markedCompleteByUserAccount(ctx context.Context, field graphql.CollectedField, obj *models.DataExchangeApproachCompletedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DataExchangeApproachCompletedActivityMeta_markedCompleteByUserAccount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.DataExchangeApproachCompletedActivityMeta().MarkedCompleteByUserAccount(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*authentication.UserAccount) + fc.Result = res + return ec.marshalNUserAccount2ᚖgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋauthenticationᚐUserAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DataExchangeApproachCompletedActivityMeta_markedCompleteByUserAccount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataExchangeApproachCompletedActivityMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_UserAccount_id(ctx, field) + case "username": + return ec.fieldContext_UserAccount_username(ctx, field) + case "isEUAID": + return ec.fieldContext_UserAccount_isEUAID(ctx, field) + case "commonName": + return ec.fieldContext_UserAccount_commonName(ctx, field) + case "locale": + return ec.fieldContext_UserAccount_locale(ctx, field) + case "email": + return ec.fieldContext_UserAccount_email(ctx, field) + case "givenName": + return ec.fieldContext_UserAccount_givenName(ctx, field) + case "familyName": + return ec.fieldContext_UserAccount_familyName(ctx, field) + case "zoneInfo": + return ec.fieldContext_UserAccount_zoneInfo(ctx, field) + case "hasLoggedIn": + return ec.fieldContext_UserAccount_hasLoggedIn(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type UserAccount", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _DateChange_isChanged(ctx context.Context, field graphql.CollectedField, obj *models.DateChange) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DateChange_isChanged(ctx, field) if err != nil { @@ -27739,6 +28050,8 @@ func (ec *executionContext) fieldContext_Mutation_updateUserNotificationPreferen return ec.fieldContext_UserNotificationPreferences_datesChanged(ctx, field) case "datesChangedNotificationType": return ec.fieldContext_UserNotificationPreferences_datesChangedNotificationType(ctx, field) + case "dataExchangeApproachCompleted": + return ec.fieldContext_UserNotificationPreferences_dataExchangeApproachCompleted(ctx, field) case "createdBy": return ec.fieldContext_UserNotificationPreferences_createdBy(ctx, field) case "createdByUserAccount": @@ -63142,6 +63455,50 @@ func (ec *executionContext) fieldContext_UserNotificationPreferences_datesChange return fc, nil } +func (ec *executionContext) _UserNotificationPreferences_dataExchangeApproachCompleted(ctx context.Context, field graphql.CollectedField, obj *models.UserNotificationPreferences) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_UserNotificationPreferences_dataExchangeApproachCompleted(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.UserNotificationPreferences().DataExchangeApproachCompleted(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]models.UserNotificationPreferenceFlag) + fc.Result = res + return ec.marshalNUserNotificationPreferenceFlag2ᚕgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐUserNotificationPreferenceFlagᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_UserNotificationPreferences_dataExchangeApproachCompleted(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "UserNotificationPreferences", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UserNotificationPreferenceFlag does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _UserNotificationPreferences_createdBy(ctx context.Context, field graphql.CollectedField, obj *models.UserNotificationPreferences) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserNotificationPreferences_createdBy(ctx, field) if err != nil { @@ -66715,6 +67072,11 @@ func (ec *executionContext) _ActivityMetaData(ctx context.Context, sel ast.Selec return graphql.Null } return ec._DatesChangedActivityMeta(ctx, sel, obj) + case *models.DataExchangeApproachCompletedActivityMeta: + if obj == nil { + return graphql.Null + } + return ec._DataExchangeApproachCompletedActivityMeta(ctx, sel, obj) default: panic(fmt.Errorf("unexpected type %T", obj)) } @@ -67978,6 +68340,96 @@ func (ec *executionContext) _DailyDigestCompleteActivityMeta(ctx context.Context return out } +var dataExchangeApproachCompletedActivityMetaImplementors = []string{"DataExchangeApproachCompletedActivityMeta", "ActivityMetaData"} + +func (ec *executionContext) _DataExchangeApproachCompletedActivityMeta(ctx context.Context, sel ast.SelectionSet, obj *models.DataExchangeApproachCompletedActivityMeta) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, dataExchangeApproachCompletedActivityMetaImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DataExchangeApproachCompletedActivityMeta") + case "version": + out.Values[i] = ec._DataExchangeApproachCompletedActivityMeta_version(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "type": + out.Values[i] = ec._DataExchangeApproachCompletedActivityMeta_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "dataExchangeApproachID": + out.Values[i] = ec._DataExchangeApproachCompletedActivityMeta_dataExchangeApproachID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "markedCompleteBy": + out.Values[i] = ec._DataExchangeApproachCompletedActivityMeta_markedCompleteBy(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "markedCompleteByUserAccount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._DataExchangeApproachCompletedActivityMeta_markedCompleteByUserAccount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var dateChangeImplementors = []string{"DateChange"} func (ec *executionContext) _DateChange(ctx context.Context, sel ast.SelectionSet, obj *models.DateChange) graphql.Marshaler { @@ -78666,6 +79118,42 @@ func (ec *executionContext) _UserNotificationPreferences(ctx context.Context, se out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "datesChangedNotificationType": out.Values[i] = ec._UserNotificationPreferences_datesChangedNotificationType(ctx, field, obj) + case "dataExchangeApproachCompleted": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._UserNotificationPreferences_dataExchangeApproachCompleted(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdBy": out.Values[i] = ec._UserNotificationPreferences_createdBy(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 1544bbc844..317a5d278e 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -34,6 +34,11 @@ func (r *dailyDigestCompleteActivityMetaResolver) AnalyzedAudits(ctx context.Con return loaders.AnalyzedAuditGetByModelPlanIDsAndDate(ctx, obj.ModelPlanIDs, obj.Date) } +// MarkedCompleteByUserAccount is the resolver for the markedCompleteByUserAccount field. +func (r *dataExchangeApproachCompletedActivityMetaResolver) MarkedCompleteByUserAccount(ctx context.Context, obj *models.DataExchangeApproachCompletedActivityMeta) (*authentication.UserAccount, error) { + return UserAccountGetByIDLOADER(ctx, obj.MarkedCompleteBy) +} + // ModelPlan is the resolver for the modelPlan field. func (r *datesChangedActivityMetaResolver) ModelPlan(ctx context.Context, obj *models.DatesChangedActivityMeta) (*models.ModelPlan, error) { return ModelPlanGetByIDLOADER(ctx, obj.ModelPlanID) @@ -107,6 +112,11 @@ func (r *Resolver) DailyDigestCompleteActivityMeta() generated.DailyDigestComple return &dailyDigestCompleteActivityMetaResolver{r} } +// DataExchangeApproachCompletedActivityMeta returns generated.DataExchangeApproachCompletedActivityMetaResolver implementation. +func (r *Resolver) DataExchangeApproachCompletedActivityMeta() generated.DataExchangeApproachCompletedActivityMetaResolver { + return &dataExchangeApproachCompletedActivityMetaResolver{r} +} + // DatesChangedActivityMeta returns generated.DatesChangedActivityMetaResolver implementation. func (r *Resolver) DatesChangedActivityMeta() generated.DatesChangedActivityMetaResolver { return &datesChangedActivityMetaResolver{r} @@ -140,6 +150,7 @@ func (r *Resolver) TaggedInPlanDiscussionActivityMeta() generated.TaggedInPlanDi type activityResolver struct{ *Resolver } type addedAsCollaboratorMetaResolver struct{ *Resolver } type dailyDigestCompleteActivityMetaResolver struct{ *Resolver } +type dataExchangeApproachCompletedActivityMetaResolver struct{ *Resolver } type datesChangedActivityMetaResolver struct{ *Resolver } type modelPlanSharedActivityMetaResolver struct{ *Resolver } type newDiscussionRepliedActivityMetaResolver struct{ *Resolver } diff --git a/pkg/graph/resolvers/added_as_collaborator_email_test.go b/pkg/graph/resolvers/added_as_collaborator_email_test.go index 5a8f5ec47a..aeef047f58 100644 --- a/pkg/graph/resolvers/added_as_collaborator_email_test.go +++ b/pkg/graph/resolvers/added_as_collaborator_email_test.go @@ -25,7 +25,7 @@ func (s *ResolverSuite) TestAddedAsCollaboratorEmail() { } expectedEmail := "CLAB.doe@local.fake" // This comes from the stub fetch user info function - testTemplate, expectedSubject, expectedBody := createAddedAsCollaboratorTemplateCacheHelper(planName, plan) + testTemplate, expectedSubject, expectedBody := createDummyTemplateCacheHelper(planName, plan) mockEmailTemplateService. EXPECT(). GetEmailTemplate(gomock.Eq(email.AddedAsCollaboratorTemplateName)). diff --git a/pkg/graph/resolvers/data_exchange_approach_completed_email_test.go b/pkg/graph/resolvers/data_exchange_approach_completed_email_test.go new file mode 100644 index 0000000000..89965b4640 --- /dev/null +++ b/pkg/graph/resolvers/data_exchange_approach_completed_email_test.go @@ -0,0 +1,65 @@ +package resolvers + +import ( + "github.com/golang/mock/gomock" + + "github.com/cmsgov/mint-app/pkg/email" + "github.com/cmsgov/mint-app/pkg/shared/oddmail" +) + +func (s *ResolverSuite) TestDataExchangeApproachCompletedEmail() { + mockController := gomock.NewController(s.T()) + mockEmailService := oddmail.NewMockEmailService(mockController) + mockEmailTemplateService := email.NewMockTemplateService(mockController) + + planName := "Plan For Milestones" + plan := s.createModelPlan(planName) + + addressBook := email.AddressBook{ + DefaultSender: "unit-test-execution@mint.cms.gov", + MINTTeamEmail: "mint.team@local.fake", + } + + testTemplate, expectedSubject, expectedBody := createDummyTemplateCacheHelper(planName, plan) + mockEmailTemplateService. + EXPECT(). + GetEmailTemplate(gomock.Eq(email.DataExchangeApproachCompletedTemplateName)). + Return(testTemplate, nil). + AnyTimes() + expectedEmail := addressBook.MINTTeamEmail + + mockEmailService. + EXPECT(). + Send( + gomock.Any(), + gomock.Eq([]string{expectedEmail}), + gomock.Any(), + gomock.Eq(expectedSubject), + gomock.Any(), + gomock.Eq(expectedBody), + ). + Times(1) + + emailServiceConfig := &oddmail.GoSimpleMailServiceConfig{ + ClientAddress: "http://localhost:3005", + } + + mockEmailService. + EXPECT(). + GetConfig(). + Return(emailServiceConfig). + AnyTimes() + + err := SendDataExchangeApproachCompletedEmailNotification( + mockEmailService, + mockEmailTemplateService, + addressBook, + plan, + expectedEmail, + "Test User", + false, + ) + s.NoError(err) + + mockController.Finish() +} diff --git a/pkg/graph/resolvers/data_exchange_approach_helper.go b/pkg/graph/resolvers/data_exchange_approach_helper.go new file mode 100644 index 0000000000..9311f6021b --- /dev/null +++ b/pkg/graph/resolvers/data_exchange_approach_helper.go @@ -0,0 +1,159 @@ +package resolvers + +import ( + "context" + + "github.com/google/uuid" + "go.uber.org/zap" + + "github.com/cmsgov/mint-app/pkg/appcontext" + "github.com/cmsgov/mint-app/pkg/email" + "github.com/cmsgov/mint-app/pkg/models" + "github.com/cmsgov/mint-app/pkg/notifications" + "github.com/cmsgov/mint-app/pkg/shared/oddmail" + "github.com/cmsgov/mint-app/pkg/sqlutils" + "github.com/cmsgov/mint-app/pkg/userhelpers" +) + +func SendDataExchangeApproachCompletedEmailNotification( + emailService oddmail.EmailService, + templateService email.TemplateService, + addressBook email.AddressBook, + modelPlan *models.ModelPlan, + userEmail string, + markedCompletedByUserCommonName string, + showFooter bool, +) error { + emailTemplate, err := templateService.GetEmailTemplate(email.DataExchangeApproachCompletedTemplateName) + if err != nil { + return err + } + + emailSubject, err := emailTemplate.GetExecutedSubject(email.DataExchangeApproachCompletedSubjectContent{ + ModelName: modelPlan.ModelName, + }) + if err != nil { + return err + } + + emailBody, err := emailTemplate.GetExecutedBody(email.DataExchangeApproachCompletedBodyContent{ + ClientAddress: emailService.GetConfig().GetClientAddress(), + ModelName: modelPlan.ModelName, + ModelID: modelPlan.GetModelPlanID().String(), + MarkedCompletedByUserCommonName: markedCompletedByUserCommonName, + ShowFooter: showFooter, + }) + if err != nil { + return err + } + + err = emailService.Send( + addressBook.DefaultSender, + []string{userEmail}, + nil, + emailSubject, + "text/html", + emailBody, + ) + if err != nil { + return err + } + return nil +} + +func SendDataExchangeApproachCompletedEmailNotifications( + ctx context.Context, + emailService oddmail.EmailService, + templateService email.TemplateService, + addressBook email.AddressBook, + receivers []*models.UserAccountAndNotificationPreferences, + modelPlan *models.ModelPlan, + markedCompletedByUserCommonName string, + showFooter bool, +) error { + for _, user := range receivers { + err := SendDataExchangeApproachCompletedEmailNotification( + emailService, + templateService, + addressBook, + modelPlan, + user.Email, + markedCompletedByUserCommonName, + showFooter, + ) + if err != nil { + return err + } + } + return nil +} + +func SendDataExchangeApproachCompletedNotification( + ctx context.Context, + emailService oddmail.EmailService, + templateService email.TemplateService, + addressBook email.AddressBook, + actorID uuid.UUID, + np sqlutils.NamedPreparer, + receivers []*models.UserAccountAndNotificationPreferences, + modelPlan *models.ModelPlan, + approach *models.DataExchangeApproach, + markedCompletedBy uuid.UUID, +) error { + logger := appcontext.ZLogger(ctx) + + emailPreferences, inAppPreferences := models.FilterNotificationPreferences(receivers) + + // Create and send in-app notifications + _, err := notifications.ActivityDataExchangeApproachCompletedCreate( + ctx, + actorID, + np, + inAppPreferences, + approach.ID, + markedCompletedBy, + ) + if err != nil { + logger.Error("failed to create and send in-app notifications", zap.Error(err)) + return err + } + + markedCompletedByUser, err := userhelpers.UserAccountGetByIDLOADER(ctx, markedCompletedBy) + if err != nil { + logger.Error("failed to get marked completed by user", zap.Error(err)) + return err + } + + // Send email to the MINTTeam email address from the address book + err = SendDataExchangeApproachCompletedEmailNotification( + emailService, + templateService, + addressBook, + modelPlan, + addressBook.MINTTeamEmail, + markedCompletedByUser.CommonName, + false, + ) + if err != nil { + logger.Error("failed to send email to MINTTeam", zap.Error(err)) + return err + } + + // Create and send email notifications + err = SendDataExchangeApproachCompletedEmailNotifications( + ctx, + emailService, + templateService, + addressBook, + emailPreferences, + modelPlan, + markedCompletedByUser.CommonName, + true, + ) + if err != nil { + logger.Error("failed to send email notifications", zap.Error(err)) + return err + } + + return nil +} diff --git a/pkg/graph/resolvers/plan_collaborator_test.go b/pkg/graph/resolvers/plan_collaborator_test.go index 8f8ce3fb39..f7547735f7 100644 --- a/pkg/graph/resolvers/plan_collaborator_test.go +++ b/pkg/graph/resolvers/plan_collaborator_test.go @@ -35,7 +35,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaboratorWithoutNotification() { } expectedEmail := "CLAB.doe@local.fake" //comes from stubFetchUserInfo - testTemplate, expectedSubject, expectedBody := createAddedAsCollaboratorTemplateCacheHelper(planName, plan) + testTemplate, expectedSubject, expectedBody := createDummyTemplateCacheHelper(planName, plan) mockEmailTemplateService. EXPECT(). @@ -120,7 +120,7 @@ func (suite *ResolverSuite) TestCreatePlanCollaboratorWithNotification() { } expectedEmail := "CLAB.doe@local.fake" //comes from stubFetchUserInfo - testTemplate, expectedSubject, expectedBody := createAddedAsCollaboratorTemplateCacheHelper(planName, plan) + testTemplate, expectedSubject, expectedBody := createDummyTemplateCacheHelper(planName, plan) mockEmailTemplateService. EXPECT(). diff --git a/pkg/graph/resolvers/resolver_test.go b/pkg/graph/resolvers/resolver_test.go index 4749b14231..59d84e453c 100644 --- a/pkg/graph/resolvers/resolver_test.go +++ b/pkg/graph/resolvers/resolver_test.go @@ -136,7 +136,7 @@ func (suite *ResolverSuite) createPlanCollaborator(mp *models.ModelPlan, userNam Return(emailServiceConfig). AnyTimes() - testTemplate, expectedSubject, expectedBody := createAddedAsCollaboratorTemplateCacheHelper(mp.ModelName, mp) + testTemplate, expectedSubject, expectedBody := createDummyTemplateCacheHelper(mp.ModelName, mp) mockEmailTemplateService. EXPECT(). GetEmailTemplate(gomock.Eq(email.AddedAsCollaboratorTemplateName)). diff --git a/pkg/graph/resolvers/resolver_test_utilities.go b/pkg/graph/resolvers/resolver_test_utilities.go index 3b394d324c..ac3dff7499 100644 --- a/pkg/graph/resolvers/resolver_test_utilities.go +++ b/pkg/graph/resolvers/resolver_test_utilities.go @@ -129,7 +129,7 @@ func getTestPrincipal(store *storage.Store, userName string) *authentication.App } -func createAddedAsCollaboratorTemplateCacheHelper( +func createDummyTemplateCacheHelper( planName string, plan *models.ModelPlan) (*emailTemplates.EmailTemplate, string, string) { templateCache := emailTemplates.NewTemplateCache() diff --git a/pkg/graph/resolvers/user_notification_preferences.resolvers.go b/pkg/graph/resolvers/user_notification_preferences.resolvers.go index d8d9ec3bfb..22c49ebc97 100644 --- a/pkg/graph/resolvers/user_notification_preferences.resolvers.go +++ b/pkg/graph/resolvers/user_notification_preferences.resolvers.go @@ -60,6 +60,11 @@ func (r *userNotificationPreferencesResolver) DatesChanged(ctx context.Context, return obj.DatesChanged, nil } +// DataExchangeApproachCompleted is the resolver for the dataExchangeApproachCompleted field. +func (r *userNotificationPreferencesResolver) DataExchangeApproachCompleted(ctx context.Context, obj *models.UserNotificationPreferences) ([]models.UserNotificationPreferenceFlag, error) { + return obj.DataExchangeApproachCompleted, nil +} + // UserNotificationPreferences returns generated.UserNotificationPreferencesResolver implementation. func (r *Resolver) UserNotificationPreferences() generated.UserNotificationPreferencesResolver { return &userNotificationPreferencesResolver{r} diff --git a/pkg/graph/schema/types/activity.graphql b/pkg/graph/schema/types/activity.graphql index ae2bc9ae56..e9ace5cbf6 100644 --- a/pkg/graph/schema/types/activity.graphql +++ b/pkg/graph/schema/types/activity.graphql @@ -10,6 +10,7 @@ enum ActivityType { MODEL_PLAN_SHARED NEW_MODEL_PLAN DATES_CHANGED + DATA_EXCHANGE_APPROACH_COMPLETED } enum DateChangeFieldType { @@ -24,7 +25,7 @@ enum DateChangeFieldType { """ ActivityMetaData is a type that represents all the data that can be captured in an Activity """ -union ActivityMetaData = TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | DailyDigestCompleteActivityMeta | NewDiscussionRepliedActivityMeta | AddedAsCollaboratorMeta | ModelPlanSharedActivityMeta | NewModelPlanActivityMeta | DatesChangedActivityMeta +union ActivityMetaData = TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | DailyDigestCompleteActivityMeta | NewDiscussionRepliedActivityMeta | AddedAsCollaboratorMeta | ModelPlanSharedActivityMeta | NewModelPlanActivityMeta | DatesChangedActivityMeta | DataExchangeApproachCompletedActivityMeta type AddedAsCollaboratorMeta { version: Int! @@ -113,6 +114,14 @@ type DatesChangedActivityMeta { dateChanges: [DateChange!]! } +type DataExchangeApproachCompletedActivityMeta { + version: Int! + type: ActivityType! + dataExchangeApproachID: UUID! + markedCompleteBy: UUID! + markedCompleteByUserAccount: UserAccount! +} + """ Activity represents an event that happened in the application that could result in a notification. """ diff --git a/pkg/graph/schema/types/user_notification_preferences.graphql b/pkg/graph/schema/types/user_notification_preferences.graphql index 5bcb3d5d1e..814db3c6ee 100644 --- a/pkg/graph/schema/types/user_notification_preferences.graphql +++ b/pkg/graph/schema/types/user_notification_preferences.graphql @@ -34,6 +34,8 @@ type UserNotificationPreferences { datesChanged: [UserNotificationPreferenceFlag!]! datesChangedNotificationType: DatesChangedNotificationType + dataExchangeApproachCompleted: [UserNotificationPreferenceFlag!]! + createdBy: UUID! createdByUserAccount: UserAccount! createdDts: Time! @@ -64,6 +66,8 @@ input UserNotificationPreferencesChanges @goModel(model: "map[string]interface{} datesChanged: [UserNotificationPreferenceFlag!] datesChangedNotificationType: DatesChangedNotificationType + + dataExchangeApproachCompleted: [UserNotificationPreferenceFlag!] } extend type Mutation { diff --git a/pkg/models/activity.go b/pkg/models/activity.go index 4eff8cac49..938ee6fadc 100644 --- a/pkg/models/activity.go +++ b/pkg/models/activity.go @@ -9,14 +9,15 @@ type ActivityType string // These constants represent the different values of ActivityType const ( - ActivityDigest ActivityType = "DAILY_DIGEST_COMPLETE" - ActivityAddedAsCollaborator ActivityType = "ADDED_AS_COLLABORATOR" - ActivityTaggedInDiscussion ActivityType = "TAGGED_IN_DISCUSSION" - ActivityTaggedInDiscussionReply ActivityType = "TAGGED_IN_DISCUSSION_REPLY" - ActivityNewDiscussionReply ActivityType = "NEW_DISCUSSION_REPLY" - ActivityModelPlanShared ActivityType = "MODEL_PLAN_SHARED" - ActivityNewModelPlan ActivityType = "NEW_MODEL_PLAN" - ActivityDatesChanged ActivityType = "DATES_CHANGED" + ActivityDigest ActivityType = "DAILY_DIGEST_COMPLETE" + ActivityAddedAsCollaborator ActivityType = "ADDED_AS_COLLABORATOR" + ActivityTaggedInDiscussion ActivityType = "TAGGED_IN_DISCUSSION" + ActivityTaggedInDiscussionReply ActivityType = "TAGGED_IN_DISCUSSION_REPLY" + ActivityNewDiscussionReply ActivityType = "NEW_DISCUSSION_REPLY" + ActivityModelPlanShared ActivityType = "MODEL_PLAN_SHARED" + ActivityNewModelPlan ActivityType = "NEW_MODEL_PLAN" + ActivityDatesChanged ActivityType = "DATES_CHANGED" + ActivityDataExchangeApproachCompleted ActivityType = "DATA_EXCHANGE_APPROACH_COMPLETED" ) // Activity represents a discrete event that has happened in the application that might be notifiable. diff --git a/pkg/models/data_exchange_approach.go b/pkg/models/data_exchange_approach.go new file mode 100644 index 0000000000..443394fce0 --- /dev/null +++ b/pkg/models/data_exchange_approach.go @@ -0,0 +1,24 @@ +package models + +import ( + "github.com/google/uuid" +) + +// DataExchangeApproach represents the data exchange approach of a model plan +type DataExchangeApproach struct { + baseStruct + modelPlanRelation + Name string `json:"name" db:"name"` + Description *string `json:"description" db:"description"` + IsComplete bool `json:"isComplete" db:"is_complete"` +} + +// NewDataExchangeApproach creates a new DataExchangeApproach with the required fields +func NewDataExchangeApproach(name string, createdBy uuid.UUID, modelPlanID uuid.UUID) *DataExchangeApproach { + return &DataExchangeApproach{ + baseStruct: NewBaseStruct(createdBy), + modelPlanRelation: NewModelPlanRelation(modelPlanID), + Name: name, + IsComplete: false, + } +} diff --git a/pkg/models/data_exchange_approach_completed_activity_meta.go b/pkg/models/data_exchange_approach_completed_activity_meta.go new file mode 100644 index 0000000000..d2aafc609b --- /dev/null +++ b/pkg/models/data_exchange_approach_completed_activity_meta.go @@ -0,0 +1,56 @@ +package models + +import ( + "database/sql/driver" + + "github.com/google/uuid" +) + +// DataExchangeApproachCompletedActivityMeta represents the notification data that +// is relevant to completing a Data Exchange Approach +type DataExchangeApproachCompletedActivityMeta struct { + ActivityMetaBaseStruct + DataExchangeApproachID uuid.UUID `json:"dataExchangeApproachID"` + MarkedCompleteBy uuid.UUID `json:"markedCompleteBy"` +} + +// newDataExchangeApproachCompletedActivityMeta creates a new DataExchangeApproachCompletedActivityMeta +func newDataExchangeApproachCompletedActivityMeta( + dataExchangeApproachID uuid.UUID, + markedCompleteBy uuid.UUID, +) *DataExchangeApproachCompletedActivityMeta { + version := 0 // iterate this if this type ever updates + return &DataExchangeApproachCompletedActivityMeta{ + ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityDataExchangeApproachCompleted, version), + DataExchangeApproachID: dataExchangeApproachID, + MarkedCompleteBy: markedCompleteBy, + } +} + +// NewDataExchangeApproachCompletedActivity creates a new Data Exchange Approach Complete type of Activity +func NewDataExchangeApproachCompletedActivity( + actorID uuid.UUID, + dataExchangeApproachID uuid.UUID, + markedCompleteBy uuid.UUID, +) *Activity { + return &Activity{ + baseStruct: NewBaseStruct(actorID), + ActorID: actorID, + EntityID: markedCompleteBy, + ActivityType: ActivityDataExchangeApproachCompleted, + MetaData: newDataExchangeApproachCompletedActivityMeta( + dataExchangeApproachID, + markedCompleteBy, + ), + } +} + +// Value allows us to satisfy the valuer interface so we can write to the database +func (d DataExchangeApproachCompletedActivityMeta) Value() (driver.Value, error) { + return GenericValue(d) +} + +// Scan implements the scanner interface so we can translate the JSONb from the db to an object in GO +func (d *DataExchangeApproachCompletedActivityMeta) Scan(src interface{}) error { + return GenericScan(src, d) +} diff --git a/pkg/models/user_notification_preferences.go b/pkg/models/user_notification_preferences.go index d45aca42f5..87aa2d0236 100644 --- a/pkg/models/user_notification_preferences.go +++ b/pkg/models/user_notification_preferences.go @@ -15,15 +15,16 @@ type UserNotificationPreferences struct { // The id of the user this preferences object is for UserID uuid.UUID `json:"userID" db:"user_id"` - DailyDigestComplete UserNotificationPreferenceFlags `json:"dailyDigestComplete" db:"daily_digest_complete"` - AddedAsCollaborator UserNotificationPreferenceFlags `json:"addedAsCollaborator" db:"added_as_collaborator"` - TaggedInDiscussion UserNotificationPreferenceFlags `json:"taggedInDiscussion" db:"tagged_in_discussion"` - TaggedInDiscussionReply UserNotificationPreferenceFlags `json:"taggedInDiscussionReply" db:"tagged_in_discussion_reply"` - NewDiscussionReply UserNotificationPreferenceFlags `json:"newDiscussionReply" db:"new_discussion_reply"` - ModelPlanShared UserNotificationPreferenceFlags `json:"modelPlanShared" db:"model_plan_shared"` - NewModelPlan UserNotificationPreferenceFlags `json:"newModelPlan" db:"new_model_plan"` - DatesChanged UserNotificationPreferenceFlags `json:"datesChanged" db:"dates_changed"` - DatesChangedNotificationType *DatesChangedNotificationType `json:"datesChangedNotificationType" db:"dates_changed_notification_type"` + DailyDigestComplete UserNotificationPreferenceFlags `json:"dailyDigestComplete" db:"daily_digest_complete"` + AddedAsCollaborator UserNotificationPreferenceFlags `json:"addedAsCollaborator" db:"added_as_collaborator"` + TaggedInDiscussion UserNotificationPreferenceFlags `json:"taggedInDiscussion" db:"tagged_in_discussion"` + TaggedInDiscussionReply UserNotificationPreferenceFlags `json:"taggedInDiscussionReply" db:"tagged_in_discussion_reply"` + NewDiscussionReply UserNotificationPreferenceFlags `json:"newDiscussionReply" db:"new_discussion_reply"` + ModelPlanShared UserNotificationPreferenceFlags `json:"modelPlanShared" db:"model_plan_shared"` + NewModelPlan UserNotificationPreferenceFlags `json:"newModelPlan" db:"new_model_plan"` + DatesChanged UserNotificationPreferenceFlags `json:"datesChanged" db:"dates_changed"` + DatesChangedNotificationType *DatesChangedNotificationType `json:"datesChangedNotificationType" db:"dates_changed_notification_type"` + DataExchangeApproachCompleted UserNotificationPreferenceFlags `json:"dataExchangeApproachCompleted" db:"data_exchange_approach_completed"` } // NewUserNotificationPreferences returns a New UserNotificationPreferences @@ -32,14 +33,15 @@ func NewUserNotificationPreferences(userID uuid.UUID) *UserNotificationPreferenc baseStruct: NewBaseStruct(userID), UserID: userID, - DailyDigestComplete: DefaultUserNotificationPreferencesFlags(), - AddedAsCollaborator: DefaultUserNotificationPreferencesFlags(), - TaggedInDiscussion: DefaultUserNotificationPreferencesFlags(), - TaggedInDiscussionReply: DefaultUserNotificationPreferencesFlags(), - NewDiscussionReply: DefaultUserNotificationPreferencesFlags(), - ModelPlanShared: DefaultUserNotificationPreferencesFlags(), - NewModelPlan: EmptyUserNotificationPreferencesFlags(), - DatesChanged: EmptyUserNotificationPreferencesFlags(), + DailyDigestComplete: DefaultUserNotificationPreferencesFlags(), + AddedAsCollaborator: DefaultUserNotificationPreferencesFlags(), + TaggedInDiscussion: DefaultUserNotificationPreferencesFlags(), + TaggedInDiscussionReply: DefaultUserNotificationPreferencesFlags(), + NewDiscussionReply: DefaultUserNotificationPreferencesFlags(), + ModelPlanShared: DefaultUserNotificationPreferencesFlags(), + NewModelPlan: EmptyUserNotificationPreferencesFlags(), + DatesChanged: EmptyUserNotificationPreferencesFlags(), + DataExchangeApproachCompleted: EmptyUserNotificationPreferencesFlags(), } } diff --git a/pkg/notifications/activity.go b/pkg/notifications/activity.go index 5bcaf095e5..8496cfc1ec 100644 --- a/pkg/notifications/activity.go +++ b/pkg/notifications/activity.go @@ -111,6 +111,7 @@ func parseRawActivityMetaData(activityType models.ActivityType, rawMetaDataJSON return nil, err } return &meta, nil + case models.ActivityAddedAsCollaborator: // Deserialize the raw JSON into AddedAsCollaboratorMeta meta := models.AddedAsCollaboratorMeta{} @@ -144,6 +145,14 @@ func parseRawActivityMetaData(activityType models.ActivityType, rawMetaDataJSON } return &meta, nil + case models.ActivityDataExchangeApproachCompleted: + // Deserialize the raw JSON into DataExchangeApproachCompletedActivityMeta + meta := models.DataExchangeApproachCompletedActivityMeta{} + if err := json.Unmarshal(rawData, &meta); err != nil { + return nil, err + } + return &meta, nil + default: // Return a default implementation or handle unsupported types return nil, fmt.Errorf("unsupported activity type: %s", activityType) diff --git a/pkg/notifications/data_exchange_approach_completed.go b/pkg/notifications/data_exchange_approach_completed.go new file mode 100644 index 0000000000..d888a185c0 --- /dev/null +++ b/pkg/notifications/data_exchange_approach_completed.go @@ -0,0 +1,41 @@ +package notifications + +import ( + "context" + + "github.com/google/uuid" + + "github.com/cmsgov/mint-app/pkg/models" + "github.com/cmsgov/mint-app/pkg/sqlutils" +) + +// ActivityDataExchangeApproachCompletedCreate creates an activity for when a data exchange approach is completed +func ActivityDataExchangeApproachCompletedCreate( + ctx context.Context, + actorID uuid.UUID, + np sqlutils.NamedPreparer, + receivers []*models.UserAccountAndNotificationPreferences, + approachID uuid.UUID, + markedCompletedBy uuid.UUID, +) (*models.Activity, error) { + + activity := models.NewDataExchangeApproachCompletedActivity( + actorID, + approachID, + markedCompletedBy, + ) + + retActivity, actErr := activityCreate(ctx, np, activity) + if actErr != nil { + return nil, actErr + } + + for _, receiver := range receivers { + _, err := userNotificationCreate(ctx, np, retActivity, receiver.ID, receiver.PreferenceFlags) + if err != nil { + return nil, err + } + } + + return retActivity, nil +} diff --git a/pkg/notifications/data_exchange_approach_completed_test.go b/pkg/notifications/data_exchange_approach_completed_test.go new file mode 100644 index 0000000000..4455ebafa5 --- /dev/null +++ b/pkg/notifications/data_exchange_approach_completed_test.go @@ -0,0 +1,62 @@ +package notifications + +import ( + "github.com/google/uuid" + + "github.com/cmsgov/mint-app/pkg/models" +) + +func (suite *NotificationsSuite) TestActivityDataExchangeApproachCompletedCreate() { + + // we are just choosing a valid UUID to set for the entityID + modelPlanID := uuid.New() + actorID := suite.testConfigs.Principal.Account().ID + + approachCreator, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, "FAKE") + suite.NoError(err) + + approachMarkedCompleteBy, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, "MINT") + suite.NoError(err) + + receivers := []*models.UserAccountAndNotificationPreferences{ + { + UserAccount: *approachCreator.UserAccount, + PreferenceFlags: models.DefaultUserNotificationPreferencesFlags(), + }, + } + + approach := models.NewDataExchangeApproach( + "TestDataExchangeApproach", + actorID, + modelPlanID, + ) + + testActivity, err := ActivityDataExchangeApproachCompletedCreate( + suite.testConfigs.Context, + actorID, + suite.testConfigs.Store, + receivers, + approach.ID, + approachMarkedCompleteBy.Account().ID, + ) + + suite.NoError(err) + suite.NotNil(testActivity) + suite.EqualValues(models.ActivityDataExchangeApproachCompleted, testActivity.ActivityType) + // Assert meta data is not deserialized here + suite.Nil(testActivity.MetaData) + //Assert meta data can be deserialized + suite.NotNil(testActivity.MetaDataRaw) + meta, err := parseRawActivityMetaData(testActivity.ActivityType, testActivity.MetaDataRaw) + suite.NoError(err) + suite.NotNil(meta) + + actorNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Principal) + suite.NoError(err) + suite.EqualValues(0, actorNots.NumUnreadNotifications()) + + creatorNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, approachCreator) + suite.NoError(err) + suite.EqualValues(1, creatorNots.NumUnreadNotifications()) + +} diff --git a/pkg/sqlqueries/SQL/user_notification_preferences/create.sql b/pkg/sqlqueries/SQL/user_notification_preferences/create.sql index 45ce0bd0b6..7db18c55b2 100644 --- a/pkg/sqlqueries/SQL/user_notification_preferences/create.sql +++ b/pkg/sqlqueries/SQL/user_notification_preferences/create.sql @@ -10,6 +10,7 @@ INSERT INTO public.user_notification_preferences( new_model_plan, dates_changed, dates_changed_notification_type, + data_exchange_approach_completed, created_by ) VALUES ( @@ -24,6 +25,7 @@ VALUES ( :new_model_plan, :dates_changed, :dates_changed_notification_type, + :data_exchange_approach_completed, :created_by ) RETURNING @@ -38,6 +40,7 @@ model_plan_shared, new_model_plan, dates_changed, dates_changed_notification_type, +data_exchange_approach_completed, created_by, created_dts, modified_by, diff --git a/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id.sql b/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id.sql index a759fdb15c..0973be8b23 100644 --- a/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id.sql +++ b/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id.sql @@ -10,6 +10,7 @@ SELECT new_model_plan, dates_changed, dates_changed_notification_type, + data_exchange_approach_completed, created_by, created_dts, modified_by, diff --git a/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id_loader.sql b/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id_loader.sql index 3ef3d5d8d5..d3a2d7982c 100644 --- a/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id_loader.sql +++ b/pkg/sqlqueries/SQL/user_notification_preferences/get_by_user_id_loader.sql @@ -18,6 +18,7 @@ SELECT UNP.new_model_plan, UNP.dates_changed, UNP.dates_changed_notification_type, + UNP.data_exchange_approach_completed, UNP.created_by, UNP.created_dts, UNP.modified_by, diff --git a/pkg/sqlqueries/SQL/user_notification_preferences/update.sql b/pkg/sqlqueries/SQL/user_notification_preferences/update.sql index f11555b3cf..96d8b5c96c 100644 --- a/pkg/sqlqueries/SQL/user_notification_preferences/update.sql +++ b/pkg/sqlqueries/SQL/user_notification_preferences/update.sql @@ -9,6 +9,7 @@ SET new_model_plan = :new_model_plan, dates_changed = :dates_changed, dates_changed_notification_type = :dates_changed_notification_type, + data_exchange_approach_completed = :data_exchange_approach_completed, modified_by = :modified_by, modified_dts = CURRENT_TIMESTAMP WHERE @@ -25,6 +26,7 @@ model_plan_shared, new_model_plan, dates_changed, dates_changed_notification_type, +data_exchange_approach_completed, created_by, created_dts, modified_by, diff --git a/src/gql/gen/graphql.ts b/src/gql/gen/graphql.ts index d53d8d6b0d..e9b77df399 100644 --- a/src/gql/gen/graphql.ts +++ b/src/gql/gen/graphql.ts @@ -56,12 +56,13 @@ export type Activity = { }; /** ActivityMetaData is a type that represents all the data that can be captured in an Activity */ -export type ActivityMetaData = AddedAsCollaboratorMeta | DailyDigestCompleteActivityMeta | DatesChangedActivityMeta | ModelPlanSharedActivityMeta | NewDiscussionRepliedActivityMeta | NewModelPlanActivityMeta | TaggedInDiscussionReplyActivityMeta | TaggedInPlanDiscussionActivityMeta; +export type ActivityMetaData = AddedAsCollaboratorMeta | DailyDigestCompleteActivityMeta | DataExchangeApproachCompletedActivityMeta | DatesChangedActivityMeta | ModelPlanSharedActivityMeta | NewDiscussionRepliedActivityMeta | NewModelPlanActivityMeta | TaggedInDiscussionReplyActivityMeta | TaggedInPlanDiscussionActivityMeta; /** ActivityType represents the possible activities that happen in application that might result in a notification */ export enum ActivityType { ADDED_AS_COLLABORATOR = 'ADDED_AS_COLLABORATOR', DAILY_DIGEST_COMPLETE = 'DAILY_DIGEST_COMPLETE', + DATA_EXCHANGE_APPROACH_COMPLETED = 'DATA_EXCHANGE_APPROACH_COMPLETED', DATES_CHANGED = 'DATES_CHANGED', MODEL_PLAN_SHARED = 'MODEL_PLAN_SHARED', NEW_DISCUSSION_REPLY = 'NEW_DISCUSSION_REPLY', @@ -291,6 +292,15 @@ export type DailyDigestCompleteActivityMeta = { version: Scalars['Int']['output']; }; +export type DataExchangeApproachCompletedActivityMeta = { + __typename: 'DataExchangeApproachCompletedActivityMeta'; + dataExchangeApproachID: Scalars['UUID']['output']; + markedCompleteBy: Scalars['UUID']['output']; + markedCompleteByUserAccount: UserAccount; + type: ActivityType; + version: Scalars['Int']['output']; +}; + export enum DataForMonitoringType { CLINICAL_DATA = 'CLINICAL_DATA', ENCOUNTER_DATA = 'ENCOUNTER_DATA', @@ -3000,6 +3010,7 @@ export type UserNotificationPreferences = { createdByUserAccount: UserAccount; createdDts: Scalars['Time']['output']; dailyDigestComplete: Array; + dataExchangeApproachCompleted: Array; datesChanged: Array; datesChangedNotificationType?: Maybe; id: Scalars['UUID']['output']; @@ -3018,6 +3029,7 @@ export type UserNotificationPreferences = { export type UserNotificationPreferencesChanges = { addedAsCollaborator?: InputMaybe>; dailyDigestComplete?: InputMaybe>; + dataExchangeApproachCompleted?: InputMaybe>; datesChanged?: InputMaybe>; datesChangedNotificationType?: InputMaybe; modelPlanShared?: InputMaybe>; @@ -3683,7 +3695,7 @@ export type GetNotificationSettingsQuery = { __typename: 'Query', currentUser: { export type GetNotificationsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetNotificationsQuery = { __typename: 'Query', currentUser: { __typename: 'CurrentUser', notifications: { __typename: 'UserNotifications', numUnreadNotifications: number, notifications: Array<{ __typename: 'UserNotification', id: UUID, isRead: boolean, inAppSent: boolean, emailSent: boolean, createdDts: Time, activity: { __typename: 'Activity', activityType: ActivityType, entityID: UUID, actorID: UUID, actorUserAccount: { __typename: 'UserAccount', commonName: string }, metaData: { __typename: 'AddedAsCollaboratorMeta', version: number, type: ActivityType, modelPlanID: UUID, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'DailyDigestCompleteActivityMeta', version: number, type: ActivityType, modelPlanIDs: Array, date: Time, analyzedAudits: Array<{ __typename: 'AnalyzedAudit', id: UUID, modelPlanID: UUID, modelName: string, date: Time, changes: { __typename: 'AnalyzedAuditChange', modelPlan?: { __typename: 'AnalyzedModelPlan', oldName?: string | null, statusChanges?: Array | null } | null, documents?: { __typename: 'AnalyzedDocuments', count?: number | null } | null, crTdls?: { __typename: 'AnalyzedCrTdls', activity?: boolean | null } | null, planSections?: { __typename: 'AnalyzedPlanSections', updated: Array, readyForReview: Array, readyForClearance: Array } | null, modelLeads?: { __typename: 'AnalyzedModelLeads', added: Array<{ __typename: 'AnalyzedModelLeadInfo', id: UUID, commonName: string }> } | null, planDiscussions?: { __typename: 'AnalyzedPlanDiscussions', activity?: boolean | null } | null } }> } | { __typename: 'DatesChangedActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, modelPlan: { __typename: 'ModelPlan', modelName: string }, dateChanges: Array<{ __typename: 'DateChange', isChanged: boolean, field: DateChangeFieldType, isRange: boolean, oldDate?: Time | null, newDate?: Time | null, oldRangeStart?: Time | null, oldRangeEnd?: Time | null, newRangeStart?: Time | null, newRangeEnd?: Time | null }> } | { __typename: 'ModelPlanSharedActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, optionalMessage?: string | null, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'NewDiscussionRepliedActivityMeta', version: number, type: ActivityType, discussionID: UUID, replyID: UUID, modelPlanID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'NewModelPlanActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'TaggedInDiscussionReplyActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, replyID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'TaggedInPlanDiscussionActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } } }> } } }; +export type GetNotificationsQuery = { __typename: 'Query', currentUser: { __typename: 'CurrentUser', notifications: { __typename: 'UserNotifications', numUnreadNotifications: number, notifications: Array<{ __typename: 'UserNotification', id: UUID, isRead: boolean, inAppSent: boolean, emailSent: boolean, createdDts: Time, activity: { __typename: 'Activity', activityType: ActivityType, entityID: UUID, actorID: UUID, actorUserAccount: { __typename: 'UserAccount', commonName: string }, metaData: { __typename: 'AddedAsCollaboratorMeta', version: number, type: ActivityType, modelPlanID: UUID, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'DailyDigestCompleteActivityMeta', version: number, type: ActivityType, modelPlanIDs: Array, date: Time, analyzedAudits: Array<{ __typename: 'AnalyzedAudit', id: UUID, modelPlanID: UUID, modelName: string, date: Time, changes: { __typename: 'AnalyzedAuditChange', modelPlan?: { __typename: 'AnalyzedModelPlan', oldName?: string | null, statusChanges?: Array | null } | null, documents?: { __typename: 'AnalyzedDocuments', count?: number | null } | null, crTdls?: { __typename: 'AnalyzedCrTdls', activity?: boolean | null } | null, planSections?: { __typename: 'AnalyzedPlanSections', updated: Array, readyForReview: Array, readyForClearance: Array } | null, modelLeads?: { __typename: 'AnalyzedModelLeads', added: Array<{ __typename: 'AnalyzedModelLeadInfo', id: UUID, commonName: string }> } | null, planDiscussions?: { __typename: 'AnalyzedPlanDiscussions', activity?: boolean | null } | null } }> } | { __typename: 'DataExchangeApproachCompletedActivityMeta' } | { __typename: 'DatesChangedActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, modelPlan: { __typename: 'ModelPlan', modelName: string }, dateChanges: Array<{ __typename: 'DateChange', isChanged: boolean, field: DateChangeFieldType, isRange: boolean, oldDate?: Time | null, newDate?: Time | null, oldRangeStart?: Time | null, oldRangeEnd?: Time | null, newRangeStart?: Time | null, newRangeEnd?: Time | null }> } | { __typename: 'ModelPlanSharedActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, optionalMessage?: string | null, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'NewDiscussionRepliedActivityMeta', version: number, type: ActivityType, discussionID: UUID, replyID: UUID, modelPlanID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'NewModelPlanActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'TaggedInDiscussionReplyActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, replyID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } | { __typename: 'TaggedInPlanDiscussionActivityMeta', version: number, type: ActivityType, modelPlanID: UUID, discussionID: UUID, content: string, modelPlan: { __typename: 'ModelPlan', modelName: string } } } }> } } }; export type GetPollNotificationsQueryVariables = Exact<{ [key: string]: never; }>;