Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EASI-4521 [Async GRB] Async presentation links #2932

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
69e653a
Init feature branch
adamodd Dec 18, 2024
e893ac5
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Dec 18, 2024
81866fb
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
ClayBenson94 Dec 19, 2024
2e687f2
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Dec 23, 2024
0e24e21
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Dec 26, 2024
960d0a0
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
ClayBenson94 Jan 6, 2025
dd00924
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 8, 2025
2e2619d
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 13, 2025
b6d80a2
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 14, 2025
a71ca4a
[EASI-4527] add presentation links schema (#2951)
mynar7 Jan 15, 2025
49ac985
[EASI-4529] notification emails (#2947)
samoddball Jan 15, 2025
a804487
Easi 4697/delete links mutation (#2953)
samoddball Jan 17, 2025
881c7d3
[EASI-4711] Remove required presentation links fields (#2954)
mynar7 Jan 17, 2025
c1afbd4
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 21, 2025
166851d
Easi 4528/query links (#2957)
samoddball Jan 22, 2025
3e4bb85
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 27, 2025
dab39eb
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 27, 2025
1bea7af
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 28, 2025
603957a
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 28, 2025
54dd6b2
Merge branch 'main' into feature/EASI-4521_grb_presentation_links
samoddball Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions EASI.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,72 @@
}
},
"response": []
},
{
"name": "Get GRB Presentation Links",
"event": [
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "graphql",
"graphql": {
"query": "query getSystemIntakeGRBReviewers($systemIntakeID: UUID!) {\n systemIntake(id: $systemIntakeID) {\n id\n grbPresentationLinks {\n systemIntakeID\n createdAt\n createdBy\n modifiedAt\n modifiedBy\n recordingLink\n recordingPasscode\n transcriptFileName\n transcriptFileStatus\n transcriptFileURL\n presentationDeckFileName\n presentationDeckFileStatus\n presentationDeckFileURL\n }\n }\n}",
"variables": "{\r\n \"systemIntakeID\": \"5af245bc-fc54-4677-bab1-1b3e798bb43c\"\r\n}"
}
},
"url": {
"raw": "{{url}}",
"host": [
"{{url}}"
]
}
},
"response": []
},
{
"name": "Set GRB Presentation Links",
"event": [
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "graphql",
"graphql": {
"query": "mutation setSystemIntakeGRBPresentationLinks(\n $input: SystemIntakeGRBPresentationLinksInput!\n) {\n setSystemIntakeGRBPresentationLinks(input: $input) {\n systemIntakeID\n createdAt\n createdBy\n modifiedAt\n modifiedBy\n recordingLink\n recordingPasscode\n transcriptFileName\n transcriptFileStatus\n transcriptFileURL\n presentationDeckFileName\n presentationDeckFileStatus\n presentationDeckFileURL\n }\n}",
"variables": "{\r\n \"input\": {\r\n \"systemIntakeID\": \"8edb237e-ad48-49b2-91cf-8534362bc6cf\",\r\n \"recordingLink\": \"https://google.com\",\r\n \"recordingPasscode\": \"123456\"\r\n }\r\n}"
}
},
"url": {
"raw": "{{url}}",
"host": [
"{{url}}"
]
}
},
"response": []
}
]
},
Expand Down
1 change: 1 addition & 0 deletions cmd/devdata/system_intake_grb_presentation_links.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
12 changes: 12 additions & 0 deletions cmd/test_email_templates/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,4 +723,16 @@ func sendITGovEmails(ctx context.Context, client *email.Client) {
},
)
noErr(err)

err = client.SystemIntake.SendGRBReviewPresentationLinksUpdatedEmail(
ctx,
email.SendGRBReviewPresentationLinksUpdatedEmailInput{
SystemIntakeID: intakeID,
ProjectName: "Project with Presentation",
RequesterName: "Nobody",
RequesterComponent: "ABCD",
Recipients: emailNotificationRecipients.RegularRecipientEmails,
},
)
noErr(err)
}
54 changes: 54 additions & 0 deletions migrations/V199__Add_async_GRB_presentation_links.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS system_intake_grb_presentation_links (
id UUID PRIMARY KEY NOT NULL,
created_by UUID NOT NULL REFERENCES user_account(id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_by UUID REFERENCES user_account(id),
modified_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
system_intake_id UUID NOT NULL UNIQUE REFERENCES system_intakes(id),
recording_link TEXT,
recording_passcode TEXT,
transcript_link TEXT,
transcript_s3_key TEXT,
transcript_file_name TEXT,
presentation_deck_s3_key TEXT,
presentation_deck_file_name TEXT,
CONSTRAINT transcript_link_or_doc_null_check CHECK (
(
transcript_link IS NULL AND
transcript_s3_key IS NULL AND
transcript_file_name IS NULL
) OR
(
transcript_link IS NOT NULL AND
transcript_s3_key IS NULL AND
transcript_file_name IS NULL
) OR
(
transcript_link IS NULL AND
transcript_s3_key IS NOT NULL AND
transcript_file_name IS NOT NULL
)
),
CONSTRAINT presentation_deck_null_check CHECK (
(
presentation_deck_s3_key IS NULL AND
presentation_deck_file_name IS NULL
) OR
(
presentation_deck_s3_key IS NOT NULL AND
presentation_deck_file_name IS NOT NULL
)
),
CONSTRAINT one_value_present_null_check CHECK (
recording_link IS NOT NULL OR
transcript_link IS NOT NULL OR
transcript_s3_key IS NOT NULL OR
presentation_deck_s3_key IS NOT NULL
)
);

COMMENT ON CONSTRAINT transcript_link_or_doc_null_check ON system_intake_grb_presentation_links IS 'Ensures either a transcript link OR document is inserted and that the file name and s3 key are present if transcript is a document';

COMMENT ON CONSTRAINT presentation_deck_null_check ON system_intake_grb_presentation_links IS 'Ensures presentation deck file name and s3 key are both present or both null';

COMMENT ON CONSTRAINT one_value_present_null_check ON system_intake_grb_presentation_links IS 'Ensures at least one presentation link value is inserted in a row to prevent a row of only metadata';
2 changes: 2 additions & 0 deletions pkg/dataloaders/dataloaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type Dataloaders struct {
SystemIntakeGovReqFeedback *dataloadgen.Loader[uuid.UUID, []*models.GovernanceRequestFeedback]
SystemIntakeGRBReviewers *dataloadgen.Loader[uuid.UUID, []*models.SystemIntakeGRBReviewer]
SystemIntakeGRBDiscussionPosts *dataloadgen.Loader[uuid.UUID, []*models.SystemIntakeGRBReviewDiscussionPost]
SystemIntakeGRBPresentationLinks *dataloadgen.Loader[uuid.UUID, *models.SystemIntakeGRBPresentationLinks]
SystemIntakeNotes *dataloadgen.Loader[uuid.UUID, []*models.SystemIntakeNote]
SystemIntakeRelatedSystemIntakes *dataloadgen.Loader[uuid.UUID, []*models.SystemIntake]
SystemIntakeRelatedTRBRequests *dataloadgen.Loader[uuid.UUID, []*models.TRBRequest]
Expand Down Expand Up @@ -122,6 +123,7 @@ func NewDataloaders(store *storage.Store, fetchUserInfos fetchUserInfosFunc, get
SystemIntakeGovReqFeedback: dataloadgen.NewLoader(dr.batchSystemIntakeGovReqFeedbackByIntakeIDs),
SystemIntakeGRBReviewers: dataloadgen.NewLoader(dr.batchSystemIntakeGRBReviewersBySystemIntakeIDs),
SystemIntakeGRBDiscussionPosts: dataloadgen.NewLoader(dr.batchSystemIntakeGRBDiscussionPostsBySystemIntakeIDs),
SystemIntakeGRBPresentationLinks: dataloadgen.NewLoader(dr.batchSystemIntakeGRBPresentationLinksByIntakeIDs),
SystemIntakeNotes: dataloadgen.NewLoader(dr.batchSystemIntakeNotesBySystemIntakeIDs),
SystemIntakeRelatedSystemIntakes: dataloadgen.NewLoader(dr.batchRelatedSystemIntakesBySystemIntakeIDs),
SystemIntakeRelatedTRBRequests: dataloadgen.NewLoader(dr.batchRelatedTRBRequestsBySystemIntakeIDs),
Expand Down
29 changes: 29 additions & 0 deletions pkg/dataloaders/system_intake_grb_presentation_links.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dataloaders

import (
"context"
"errors"

"github.com/google/uuid"

"github.com/cms-enterprise/easi-app/pkg/helpers"
"github.com/cms-enterprise/easi-app/pkg/models"
)

func (d *dataReader) batchSystemIntakeGRBPresentationLinksByIntakeIDs(ctx context.Context, systemIntakeIDs []uuid.UUID) ([]*models.SystemIntakeGRBPresentationLinks, []error) {
data, err := d.db.SystemIntakeGRBPresentationLinksByIntakeIDs(ctx, systemIntakeIDs)
if err != nil {
return nil, []error{err}
}

return helpers.OneToOne(systemIntakeIDs, data), nil
}

func GetSystemIntakeGRBPresentationLinksByIntakeID(ctx context.Context, systemIntakeID uuid.UUID) (*models.SystemIntakeGRBPresentationLinks, error) {
loaders, ok := loadersFromCTX(ctx)
if !ok {
return nil, errors.New("unexpected nil loaders in GetSystemIntakeGRBPresentationLinksByIntakeID")
}

return loaders.SystemIntakeGRBPresentationLinks.Load(ctx, systemIntakeID)
}
8 changes: 8 additions & 0 deletions pkg/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type templates struct {
grbReviewDiscussionReply templateCaller
grbReviewDiscussionIndividualTagged templateCaller
grbReviewDiscussionGroupTagged templateCaller
grbReviewPresentationLinksUpdated templateCaller
}

// sender is an interface for swapping out email provider implementations
Expand Down Expand Up @@ -429,6 +430,13 @@ func NewClient(config Config, sender sender) (Client, error) {
}
appTemplates.grbReviewDiscussionGroupTagged = grbReviewDiscussionGroupTagged

grbReviewPresentationLinksUpdatedTemplateName := "grb_review_presentation_links_updated.gohtml"
grbReviewPresentationLinksUpdated := rawTemplates.Lookup(grbReviewPresentationLinksUpdatedTemplateName)
if grbReviewPresentationLinksUpdated == nil {
return Client{}, templateError(grbReviewPresentationLinksUpdatedTemplateName)
}
appTemplates.grbReviewPresentationLinksUpdated = grbReviewPresentationLinksUpdated

client := Client{
config: config,
templates: appTemplates,
Expand Down
2 changes: 1 addition & 1 deletion pkg/email/grb_review_discussion_group_tagged.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type GRBReviewDiscussionGroupTaggedBody struct {

func (sie systemIntakeEmails) grbReviewDiscussionGroupTaggedBody(input SendGRBReviewDiscussionGroupTaggedEmailInput) (string, error) {
if sie.client.templates.grbReviewDiscussionGroupTagged == nil {
return "", errors.New("grb review discussion reply template is nil")
return "", errors.New("grb review discussion group tagged template is nil")
}

grbReviewPath := path.Join("it-governance", input.SystemIntakeID.String(), "grb-review")
Expand Down
2 changes: 1 addition & 1 deletion pkg/email/grb_review_discussion_individual_tagged.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type GRBReviewDiscussionIndividualTaggedBody struct {

func (sie systemIntakeEmails) grbReviewDiscussionIndividualTaggedBody(input SendGRBReviewDiscussionIndividualTaggedEmailInput) (string, error) {
if sie.client.templates.grbReviewDiscussionIndividualTagged == nil {
return "", errors.New("grb review discussion reply template is nil")
return "", errors.New("grb review discussion individual tagged template is nil")
}

grbReviewPath := path.Join("it-governance", input.SystemIntakeID.String(), "grb-review")
Expand Down
70 changes: 70 additions & 0 deletions pkg/email/grb_review_presentation_links_updated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package email

import (
"bytes"
"context"
"errors"
"fmt"
"path"

"github.com/google/uuid"

"github.com/cms-enterprise/easi-app/pkg/models"
)

type SendGRBReviewPresentationLinksUpdatedEmailInput struct {
SystemIntakeID uuid.UUID
ProjectName string
RequesterName string
RequesterComponent string
Recipients []models.EmailAddress
}

type grbReviewPresentationLinksUpdatedBody struct {
ProjectName string
RequesterName string
RequesterComponent string
SystemIntakeLink string
ITGovernanceInboxAddress models.EmailAddress
}

func (sie systemIntakeEmails) grbReviewPresentationLinksUpdatedBody(input SendGRBReviewPresentationLinksUpdatedEmailInput) (string, error) {
if sie.client.templates.grbReviewPresentationLinksUpdated == nil {
return "", errors.New("grb review presentation links updated template is nil")
}

grbReviewPath := path.Join("it-governance", input.SystemIntakeID.String(), "grb-review")

data := grbReviewPresentationLinksUpdatedBody{
ProjectName: input.ProjectName,
SystemIntakeLink: sie.client.urlFromPath(grbReviewPath),
RequesterName: input.RequesterName,
RequesterComponent: input.RequesterComponent,
ITGovernanceInboxAddress: sie.client.config.GRTEmail,
}

var b bytes.Buffer
if err := sie.client.templates.grbReviewPresentationLinksUpdated.Execute(&b, data); err != nil {
return "", err
}

return b.String(), nil
}

func (sie systemIntakeEmails) SendGRBReviewPresentationLinksUpdatedEmail(ctx context.Context, input SendGRBReviewPresentationLinksUpdatedEmailInput) error {
subject := fmt.Sprintf("Presentation links updated on the GRB review for %s", input.ProjectName)

body, err := sie.grbReviewPresentationLinksUpdatedBody(input)
if err != nil {
return err
}

return sie.client.sender.Send(
ctx,
NewEmail().
WithToAddresses(input.Recipients).
WithCCAddresses([]models.EmailAddress{sie.client.config.GRTEmail}).
WithSubject(subject).
WithBody(body),
)
}
76 changes: 76 additions & 0 deletions pkg/email/grb_review_presentation_links_updated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package email

import (
"context"
"fmt"
"path"

"github.com/google/uuid"

"github.com/cms-enterprise/easi-app/pkg/models"
)

func (s *EmailTestSuite) TestSendGRBReviewPresentationLinksUpdatedEmail() {
ctx := context.Background()
intakeID := uuid.MustParse("24dd7736-e4c2-4f67-8844-51187de49069")
requestName := "Presentation Links"
requester := "Nobody"
requesterComponent := "ABCD"
recipients := []models.EmailAddress{"someone@cms.gov"}

sender := mockSender{}
client, err := NewClient(s.config, &sender)
s.NoError(err)

err = client.SystemIntake.SendGRBReviewPresentationLinksUpdatedEmail(
ctx,
SendGRBReviewPresentationLinksUpdatedEmailInput{
SystemIntakeID: intakeID,
ProjectName: requestName,
RequesterName: requester,
RequesterComponent: requesterComponent,
Recipients: recipients,
},
)
s.NoError(err)

expectedSubject := fmt.Sprintf("Presentation links updated on the GRB review for %s", requestName)
s.Equal(expectedSubject, sender.subject)

s.ElementsMatch(sender.toAddresses, recipients)
s.ElementsMatch(sender.ccAddresses, []models.EmailAddress{s.config.GRTEmail})

intakePath := path.Join("it-governance", intakeID.String(), "grb-review")

grbReviewLink := client.urlFromPath(intakePath)

expectedEmail := fmt.Sprintf(`
<h1 class="header-title">EASi</h1>
<p class="header-subtitle">Easy Access to System Information</p>

<p>The Governance Admin Team has updated the presentation recording links on the GRB review for %[1]s. You
may view the recording and/or slide deck using the link below.</p>

<p><strong><a href="%[2]s">View this request in EASi</a></strong></p>

<br>
<div class="no-margin">
<p><strong>Request summary:</strong></p>
<p>Project title: %[1]s</p>
<p>Requester: %[3]s, %[4]s</p>
</div>

<br>
<p>If you have questions, please contact the Governance Team at <a
href="mailto:%[5]s">%[5]s</a>.</p>
<hr>
<p>You will continue to receive email notifications about this request until it is closed.</p>`,
requestName,
grbReviewLink,
requester,
requesterComponent,
s.config.GRTEmail.String(),
)

s.EqualHTML(expectedEmail, sender.body)
}
Loading
Loading