diff --git a/pkg/email/model_plan_date_changed.go b/pkg/email/model_plan_date_changed.go index 2c75e4085d..953a2cfcb3 100644 --- a/pkg/email/model_plan_date_changed.go +++ b/pkg/email/model_plan_date_changed.go @@ -10,6 +10,7 @@ type ModelPlanDateChangedSubjectContent struct { // DateChange defines the parameters necessary for parsing date changes, both singular and ranges // If the OldRange and NewRange are both nil, then the change is singular type DateChange struct { + IsChanged bool Field string IsRange bool OldDate, NewDate *time.Time diff --git a/pkg/email/templates/model_plan_date_changed_body.html b/pkg/email/templates/model_plan_date_changed_body.html index dc329732c6..1f3cea2318 100644 --- a/pkg/email/templates/model_plan_date_changed_body.html +++ b/pkg/email/templates/model_plan_date_changed_body.html @@ -85,39 +85,38 @@

Dates updated for {{.ModelName}}



Anticipated high level timeline

-
- -{{define "oldDate"}} - {{if .}} - {{.Format "01/02/2006"}} - {{else}} - no date entered - {{end}} -{{end}} +
-{{define "newDate"}} +{{define "date"}} {{if .}} {{.Format "01/02/2006"}} {{else}} - no date entered + no date entered {{end}} {{end}} {{range .DateChanges}}

{{.Field}}: -
+
{{if .IsRange}} - - {{template "oldDate" .OldRangeStart}} - {{template "oldDate" .OldRangeEnd}}
-
- {{template "newDate" .NewRangeStart}} - {{template "newDate" .NewRangeEnd}} + {{if .IsChanged}} + + {{template "date" .OldRangeStart}} - {{template "date" .OldRangeEnd}}
+
+ {{template "date" .NewRangeStart}} - {{template "date" .NewRangeEnd}} + {{else}} + {{template "date" .OldRangeStart}} - {{template "date" .OldRangeEnd}} + {{end}} {{else}} - {{template "oldDate" .OldDate}}
- {{template "newDate" .NewDate}} + {{if .IsChanged}} + {{template "date" .OldDate}} - {{template "date" .NewDate}} + {{else}} + {{template "date" .OldDate}} + {{end}} {{end}} -
-
+
+

{{end}} diff --git a/pkg/graph/resolvers/plan_basics.go b/pkg/graph/resolvers/plan_basics.go index 15fffd026b..931bbefc7c 100644 --- a/pkg/graph/resolvers/plan_basics.go +++ b/pkg/graph/resolvers/plan_basics.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "time" "github.com/cmsgov/mint-app/pkg/email" "github.com/cmsgov/mint-app/pkg/shared/oddmail" @@ -118,6 +119,14 @@ func extractChangedDates(changes map[string]interface{}, existing *models.PlanBa return dateChanges, nil } +func sanitizeZeroDate(date *time.Time) *time.Time { + if date == nil || (date != nil && date.IsZero()) { + return nil + } + + return date +} + func sendDateChangedEmails( emailService oddmail.EmailService, emailTemplateService email.TemplateService, @@ -138,8 +147,20 @@ func sendDateChangedEmails( } dateChangeSlice := make([]email.DateChange, 0, len(dateChanges)) - for _, v := range dateChanges { - dateChangeSlice = append(dateChangeSlice, v) + + // Loop over the field data map to ensure order of the date changes in the email + orderedCommonKeys := getOrderedCommonKeys() + for _, commonKey := range orderedCommonKeys { + dateChange := dateChanges[commonKey] + + dateChange.OldDate = sanitizeZeroDate(dateChange.OldDate) + dateChange.NewDate = sanitizeZeroDate(dateChange.NewDate) + dateChange.OldRangeStart = sanitizeZeroDate(dateChange.OldRangeStart) + dateChange.NewRangeStart = sanitizeZeroDate(dateChange.NewRangeStart) + dateChange.OldRangeEnd = sanitizeZeroDate(dateChange.OldRangeEnd) + dateChange.NewRangeEnd = sanitizeZeroDate(dateChange.NewRangeEnd) + + dateChangeSlice = append(dateChangeSlice, dateChange) } emailBody, err := emailTemplate.GetExecutedBody(email.ModelPlanDateChangedBodyContent{ diff --git a/pkg/graph/resolvers/plan_basics_helper.go b/pkg/graph/resolvers/plan_basics_helper.go index 975596da4d..a153840e9c 100644 --- a/pkg/graph/resolvers/plan_basics_helper.go +++ b/pkg/graph/resolvers/plan_basics_helper.go @@ -65,7 +65,6 @@ func (dp *DateProcessor) ExtractChangedDates() (map[string]email.DateChange, err dateChanges := make(map[string]email.DateChange) for fieldKey, fieldData := range fieldDataMap { - isFieldChanged, oldValue, newValue := dp.checkDateFieldChanged(fieldKey) if isFieldChanged { if fieldData.IsRange { // check if the field is a range @@ -74,8 +73,9 @@ func (dp *DateProcessor) ExtractChangedDates() (map[string]email.DateChange, err } dateChangeValue := &email.DateChange{ - Field: fieldData.HumanReadableName, - IsRange: true, + IsChanged: true, + Field: fieldData.HumanReadableName, + IsRange: true, } // Determine the values for the other end of the range @@ -126,12 +126,54 @@ func (dp *DateProcessor) ExtractChangedDates() (map[string]email.DateChange, err dateChanges[fieldData.CommonKey] = *dateChangeValue } else { dateChanges[fieldKey] = email.DateChange{ - Field: fieldData.HumanReadableName, - IsRange: false, - OldDate: copyTime(oldValue), - NewDate: copyTime(newValue), + IsChanged: true, + Field: fieldData.HumanReadableName, + IsRange: false, + OldDate: copyTime(oldValue), + NewDate: copyTime(newValue), + } + } + } + } + + if len(dateChanges) == 0 { + return dateChanges, nil + } + + for fieldKey, fieldData := range fieldDataMap { + if _, isChanged := dateChanges[fieldData.CommonKey]; !isChanged { + // Only handle start of range in range dates to simplify logic + if fieldData.IsRange && !fieldData.IsRangeStart { + continue + } + + dateChange := email.DateChange{ + IsChanged: false, + Field: fieldData.HumanReadableName, + IsRange: fieldData.IsRange, + } + + if fieldData.IsRange { + if dp.existing[fieldKey] != nil { + dateChange.OldRangeStart = copyTime(dp.existing[fieldKey].(*time.Time)) + } else { + dateChange.OldRangeStart = nil + } + + if dp.existing[fieldKey] != nil { + dateChange.OldRangeEnd = copyTime(dp.existing[fieldData.OtherRangeKey].(*time.Time)) + } else { + dateChange.OldRangeEnd = nil + } + } else { + if dp.existing[fieldKey] != nil { + dateChange.OldDate = copyTime(dp.existing[fieldKey].(*time.Time)) + } else { + dateChange.OldDate = nil } } + + dateChanges[fieldData.CommonKey] = dateChange } } @@ -203,6 +245,7 @@ func getFieldDataMap() map[string]dateFieldData { "completeICIP": { HumanReadableName: "Complete ICIP", IsRange: false, + CommonKey: "completeICIP", }, "clearanceStarts": { HumanReadableName: "Clearance", @@ -221,6 +264,7 @@ func getFieldDataMap() map[string]dateFieldData { "announced": { HumanReadableName: "Announce model", IsRange: false, + CommonKey: "announced", }, "applicationsStart": { HumanReadableName: "Application period", @@ -253,7 +297,19 @@ func getFieldDataMap() map[string]dateFieldData { "wrapUpEnds": { HumanReadableName: "Model wrap-up end date", IsRange: false, + CommonKey: "wrapUpEnds", }, } return fieldData } + +func getOrderedCommonKeys() []string { + return []string{ + "completeICIP", + "clearance", + "announced", + "applications", + "performancePeriod", + "wrapUpEnds", + } +} diff --git a/pkg/graph/resolvers/plan_basics_helper_test.go b/pkg/graph/resolvers/plan_basics_helper_test.go index 5a84637d9d..da31c22032 100644 --- a/pkg/graph/resolvers/plan_basics_helper_test.go +++ b/pkg/graph/resolvers/plan_basics_helper_test.go @@ -48,8 +48,29 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: defaultExisting, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + OldDate: &t1, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + OldDate: &t1, + }, + "applications": { + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, "performancePeriod": { - Field: "Performance period", + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsChanged: true, IsRange: true, OldDate: nil, NewDate: nil, @@ -58,6 +79,10 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { NewRangeStart: &t2, NewRangeEnd: defaultExisting.PerformancePeriodEnds, }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + OldDate: &t1, + }, }, }, // Incorrect field name @@ -86,64 +111,49 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { existing: defaultExisting, expected: map[string]email.DateChange{ "completeICIP": { - Field: "Complete ICIP", - IsRange: false, - OldDate: defaultExisting.CompleteICIP, - NewDate: &t2, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: nil, + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + IsChanged: true, + OldDate: defaultExisting.CompleteICIP, + NewDate: &t2, }, "clearance": { - Field: "Clearance", + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, OldRangeStart: defaultExisting.ClearanceStarts, OldRangeEnd: defaultExisting.ClearanceEnds, NewRangeStart: &t2, NewRangeEnd: &t2, }, "announced": { - Field: "Announce model", - IsRange: false, - OldDate: defaultExisting.Announced, - NewDate: &t2, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: nil, + Field: getFieldDataMap()["announced"].HumanReadableName, + IsChanged: true, + OldDate: defaultExisting.Announced, + NewDate: &t2, }, "applications": { - Field: "Application period", + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, OldRangeStart: defaultExisting.ApplicationsStart, OldRangeEnd: defaultExisting.ApplicationsEnd, NewRangeStart: &t2, NewRangeEnd: &t2, }, "performancePeriod": { - Field: "Performance period", + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, OldRangeStart: defaultExisting.PerformancePeriodStarts, OldRangeEnd: defaultExisting.PerformancePeriodEnds, NewRangeStart: &t2, NewRangeEnd: &t2, }, "wrapUpEnds": { - Field: "Model wrap-up end date", - IsRange: false, - OldDate: defaultExisting.WrapUpEnds, - NewDate: &t2, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: nil, + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + IsChanged: true, + OldDate: defaultExisting.WrapUpEnds, + NewDate: &t2, }, }, }, @@ -165,15 +175,36 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { WrapUpEnds: nil, }, expected: map[string]email.DateChange{ + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + OldDate: &t1, + }, + "applications": { + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + OldDate: &t1, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, "wrapUpEnds": { - Field: "Model wrap-up end date", - IsRange: false, - OldDate: nil, - NewDate: &t1, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: nil, + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + IsChanged: true, + NewDate: &t1, }, }, }, @@ -185,15 +216,28 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: &models.PlanBasics{}, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + }, "announced": { - Field: "Announce model", - IsRange: false, - OldDate: nil, - NewDate: &t2, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: nil, + Field: getFieldDataMap()["announced"].HumanReadableName, + IsChanged: true, + NewDate: &t2, + }, + "applications": { + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsRange: true, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, }, }, }, @@ -205,15 +249,36 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: defaultExisting, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + OldDate: &t1, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, "announced": { - Field: "Announce model", - IsRange: false, - OldDate: &t1, - NewDate: nil, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: nil, + Field: getFieldDataMap()["announced"].HumanReadableName, + IsChanged: true, + OldDate: &t1, + }, + "applications": { + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + OldDate: &t1, }, }, }, @@ -225,15 +290,28 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: &models.PlanBasics{}, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + }, "applications": { - Field: "Application period", + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, - OldRangeStart: nil, - OldRangeEnd: nil, NewRangeStart: &t2, - NewRangeEnd: nil, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, }, }, }, @@ -245,15 +323,28 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: &models.PlanBasics{}, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + }, "applications": { - Field: "Application period", - IsRange: true, - OldDate: nil, - NewDate: nil, - OldRangeStart: nil, - OldRangeEnd: nil, - NewRangeStart: nil, - NewRangeEnd: &t2, + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, + IsRange: true, + NewRangeEnd: &t2, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, }, }, }, @@ -265,16 +356,38 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: defaultExisting, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + OldDate: &t1, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + OldDate: &t1, + }, "applications": { - Field: "Application period", + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, OldRangeStart: &t1, OldRangeEnd: &t1, - NewRangeStart: nil, NewRangeEnd: &t1, }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + OldDate: &t1, + }, }, }, // Range end date from non-nil to nil @@ -285,15 +398,37 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: defaultExisting, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + OldDate: &t1, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + OldDate: &t1, + }, "applications": { - Field: "Application period", + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, OldRangeStart: &t1, OldRangeEnd: &t1, NewRangeStart: &t1, - NewRangeEnd: nil, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + OldDate: &t1, }, }, }, @@ -306,16 +441,30 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: &models.PlanBasics{}, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + }, "applications": { - Field: "Application period", + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, IsRange: true, - OldDate: nil, - NewDate: nil, - OldRangeStart: nil, - OldRangeEnd: nil, NewRangeStart: &t2, NewRangeEnd: &t2, }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, + IsRange: true, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + }, }, }, // Range both dates from non-nil to nil @@ -327,15 +476,36 @@ func (suite *ResolverSuite) TestDateProcessorExtractChangedDates() { }, existing: defaultExisting, expected: map[string]email.DateChange{ + "completeICIP": { + Field: getFieldDataMap()["completeICIP"].HumanReadableName, + OldDate: &t1, + }, + "clearance": { + Field: getFieldDataMap()["clearanceStarts"].HumanReadableName, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "announced": { + Field: getFieldDataMap()["announced"].HumanReadableName, + OldDate: &t1, + }, "applications": { - Field: "Application period", + Field: getFieldDataMap()["applicationsStart"].HumanReadableName, + IsChanged: true, + IsRange: true, + OldRangeStart: &t1, + OldRangeEnd: &t1, + }, + "performancePeriod": { + Field: getFieldDataMap()["performancePeriodStarts"].HumanReadableName, IsRange: true, - OldDate: nil, - NewDate: nil, OldRangeStart: &t1, OldRangeEnd: &t1, - NewRangeStart: nil, - NewRangeEnd: nil, + }, + "wrapUpEnds": { + Field: getFieldDataMap()["wrapUpEnds"].HumanReadableName, + OldDate: &t1, }, }, },