Skip to content

Commit

Permalink
SW-4258 Email notifications when a user schedules/reschedules an obse…
Browse files Browse the repository at this point in the history
…rvation (#1383)

- Email notification is sent to the Terraformation Contact *only*, if one exists
- There is no headline or web link in this notification
- There are no in-app notifications
  • Loading branch information
karthikbtf authored Oct 2, 2023
1 parent 16beff4 commit d62b389
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ class OrganizationStore(
return Role.entries.associateWith { countByRoleId[it] ?: 0 }
}

/** Fetches the Terraformation Contact role user in an organization, if one exists. */
fun fetchTerraformationContact(organizationId: OrganizationId): UserId? =
dslContext
.select(ORGANIZATION_USERS.USER_ID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.terraformation.backend.daily.NotificationJobSucceededEvent
import com.terraformation.backend.db.AccessionNotFoundException
import com.terraformation.backend.db.FacilityNotFoundException
import com.terraformation.backend.db.default_schema.FacilityId
import com.terraformation.backend.db.default_schema.OrganizationId
import com.terraformation.backend.db.default_schema.Role
import com.terraformation.backend.db.seedbank.AccessionId
import com.terraformation.backend.device.db.DeviceStore
Expand All @@ -30,6 +31,8 @@ import com.terraformation.backend.email.model.EmailTemplateModel
import com.terraformation.backend.email.model.FacilityAlertRequested
import com.terraformation.backend.email.model.FacilityIdle
import com.terraformation.backend.email.model.NurserySeedlingBatchReady
import com.terraformation.backend.email.model.ObservationRescheduled
import com.terraformation.backend.email.model.ObservationScheduled
import com.terraformation.backend.email.model.ObservationStarted
import com.terraformation.backend.email.model.ObservationUpcoming
import com.terraformation.backend.email.model.ReportCreated
Expand All @@ -42,6 +45,8 @@ import com.terraformation.backend.nursery.event.NurserySeedlingBatchReadyEvent
import com.terraformation.backend.report.event.ReportCreatedEvent
import com.terraformation.backend.seedbank.event.AccessionDryingEndEvent
import com.terraformation.backend.tracking.db.PlantingSiteStore
import com.terraformation.backend.tracking.event.ObservationRescheduledEvent
import com.terraformation.backend.tracking.event.ObservationScheduledEvent
import com.terraformation.backend.tracking.event.ObservationStartedEvent
import com.terraformation.backend.tracking.event.ObservationUpcomingNotificationDueEvent
import com.terraformation.backend.tracking.model.PlantingSiteDepth
Expand Down Expand Up @@ -263,6 +268,55 @@ class EmailNotificationService(
webAppUrls.googlePlay.toString()))
}

@EventListener
fun on(event: ObservationScheduledEvent) {
val organizationId = parentStore.getOrganizationId(event.observation.id)!!
// return if we don't have a TF contact to send email to
val user = getTerraformationContactUser(organizationId) ?: return
val organization =
organizationStore.fetchOneById(organizationId, OrganizationStore.FetchDepth.Organization)
val plantingSite =
plantingSiteStore.fetchSiteById(event.observation.plantingSiteId, PlantingSiteDepth.Site)
emailService.sendUserNotification(
user,
ObservationScheduled(
config,
organization.name,
plantingSite.name,
event.observation.startDate,
event.observation.endDate,
),
false,
)
}

@EventListener
fun on(event: ObservationRescheduledEvent) {
val organizationId = parentStore.getOrganizationId(event.originalObservation.id)!!
// return if we don't have a TF contact to send email to
val user = getTerraformationContactUser(organizationId) ?: return
val organization =
organizationStore.fetchOneById(organizationId, OrganizationStore.FetchDepth.Organization)
val plantingSite =
plantingSiteStore.fetchSiteById(
event.originalObservation.plantingSiteId,
PlantingSiteDepth.Site,
)
emailService.sendUserNotification(
user,
ObservationRescheduled(
config,
organization.name,
plantingSite.name,
event.originalObservation.startDate,
event.originalObservation.endDate,
event.rescheduledObservation.startDate,
event.rescheduledObservation.endDate,
),
false,
)
}

@EventListener
fun on(@Suppress("UNUSED_PARAMETER") event: NotificationJobStartedEvent) {
pendingEmails.remove()
Expand Down Expand Up @@ -301,5 +355,11 @@ class EmailNotificationService(
return userStore.fetchByOrganizationId(organizationId)
}

private fun getTerraformationContactUser(organizationId: OrganizationId): IndividualUser? {
val tfContactId = organizationStore.fetchTerraformationContact(organizationId) ?: return null
return userStore.fetchOneById(tfContactId) as? IndividualUser
?: throw IllegalArgumentException("Terraformation Contact user must be an individual user")
}

data class EmailRequest(val user: IndividualUser, val emailTemplateModel: EmailTemplateModel)
}
50 changes: 46 additions & 4 deletions src/main/kotlin/com/terraformation/backend/email/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ abstract class EmailTemplateModel(config: TerrawareServerConfig) {

return htmlWithLinks.let { HTMLOutputFormat.INSTANCE.fromMarkup(it) }
}

fun dateString(date: LocalDate): String =
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(currentLocale()).format(date)
}

class FacilityAlertRequested(
Expand Down Expand Up @@ -219,8 +222,47 @@ class ObservationUpcoming(
get() = "observation/upcoming"

val startDateString: String
get() =
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.withLocale(currentLocale())
.format(startDate)
get() = dateString(startDate)
}

class ObservationScheduled(
config: TerrawareServerConfig,
val organizationName: String,
val plantingSiteName: String,
val startDate: LocalDate,
val endDate: LocalDate,
) : EmailTemplateModel(config) {
override val templateDir: String
get() = "observation/scheduled"

val startDateString: String
get() = dateString(startDate)

val endDateString: String
get() = dateString(endDate)
}

class ObservationRescheduled(
config: TerrawareServerConfig,
val organizationName: String,
val plantingSiteName: String,
val originalStartDate: LocalDate,
val originalEndDate: LocalDate,
val newStartDate: LocalDate,
val newEndDate: LocalDate,
) : EmailTemplateModel(config) {
override val templateDir: String
get() = "observation/rescheduled"

val originalStartDateString: String
get() = dateString(originalStartDate)

val originalEndDateString: String
get() = dateString(originalEndDate)

val newStartDateString: String
get() = dateString(newStartDate)

val newEndDateString: String
get() = dateString(newEndDate)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import com.terraformation.backend.tracking.db.ObservationRescheduleStateExceptio
import com.terraformation.backend.tracking.db.ObservationStore
import com.terraformation.backend.tracking.db.PlantingSiteStore
import com.terraformation.backend.tracking.db.ScheduleObservationWithoutPlantsException
import com.terraformation.backend.tracking.event.ObservationRescheduledEvent
import com.terraformation.backend.tracking.event.ObservationScheduledEvent
import com.terraformation.backend.tracking.event.ObservationStartedEvent
import com.terraformation.backend.tracking.model.NewObservationModel
import com.terraformation.backend.tracking.model.PlantingSiteDepth
Expand Down Expand Up @@ -160,7 +162,10 @@ class ObservationService(
throw ScheduleObservationWithoutPlantsException(observation.plantingSiteId)
}

return observationStore.createObservation(observation)
val observationId = observationStore.createObservation(observation)
eventPublisher.publishEvent(
ObservationScheduledEvent(observationStore.fetchObservationById(observationId)))
return observationId
}

fun rescheduleObservation(
Expand All @@ -179,6 +184,9 @@ class ObservationService(
}

observationStore.rescheduleObservation(observationId, startDate, endDate)
eventPublisher.publishEvent(
ObservationRescheduledEvent(
observation, observationStore.fetchObservationById(observation.id)))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ data class ObservationStartedEvent(
data class ObservationUpcomingNotificationDueEvent(
val observation: ExistingObservationModel,
)

/** Published when an observation is scheduled by an end user in Terraware. */
data class ObservationScheduledEvent(
val observation: ExistingObservationModel,
)

/** Published when an observation is rescheduled by an end user in Terraware. */
data class ObservationRescheduledEvent(
val originalObservation: ExistingObservationModel,
val rescheduledObservation: ExistingObservationModel,
)
12 changes: 12 additions & 0 deletions src/main/resources/i18n/Messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ notification.email.html.footer.2=PO Box 3470, PMB 15777
notification.email.html.footer.3=Honolulu, HI 96801-3470
notification.email.html.manageSettings=Manage your [notification settings].
notification.email.text.footer=Manage your notification settings\: {0}\n\nTerraformation Inc.\nPO Box 3470, PMB 15777, Honolulu, HI 96801-3470\n\nhttps\://twitter.com/TF_Global\nhttps\://www.linkedin.com/company/terraformation/\nhttps\://www.instagram.com/globalterraform/\nhttps\://www.facebook.com/GlobalTerraform\nhttps\://terraformation.com/
# {0} is the name of a planting site.
notification.observation.rescheduled.email.body.1=The planting site {0} has had an observation rescheduled.
# {0} and {1} are dates.
notification.observation.rescheduled.email.body.2=Previously it was scheduled for {0} through {1}.
# {0} and {1} are dates.
notification.observation.rescheduled.email.body.3=Now the next observation is scheduled for {0} through {1}.
# {0} is the name of an organization. {1} is the name of a planting site.
notification.observation.rescheduled.email.subject=Organization {0} has rescheduled an observation of planting site {1}
# {0} is the name of a planting site. {1} and {2} are dates.
notification.observation.scheduled.email.body.1=The planting site {0} has had an observation scheduled for {1} through {2}
# {0} is the name of an organization. {1} is the name of a planting site.
notification.observation.scheduled.email.subject=Organization {0} has scheduled an observation of planting site {1}
notification.observation.started.app.body=Observations of your plantings need to be completed this month.
notification.observation.started.app.title=It is time to monitor your plantings\!
notification.observation.started.email.body.1=It is time to monitor your plantings\!
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/templates/email/head.ftlh.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
color="#333025"
padding="0px 32px 24px 32px"
/>
<mj-class
name="text-body03-leading"
font-family="Inter, sans-serif"
font-weight="400"
font-size="14px"
line-height="20px"
color="#333025"
padding="24px 32px 24px 32px"
/>
<mj-class
name="btn-productive-primary-md"
background-color="#2C8658"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!-- <#-- @ftlvariable name="" type="com.terraformation.backend.email.model.ObservationRescheduled" --> -->
<!-- <#setting date_format="full"> -->
<mjml>
<mj-include path="../../head.ftlh.mjml"/>

<mj-body>
<mj-include path="../../logo.ftlh.mjml"/>

<mj-section padding="0px">
<mj-column mj-class="body-wrapper">
<mj-text mj-class="text-body03-leading">
${strings("notification.observation.rescheduled.email.body.1", plantingSiteName)}
</mj-text>
<mj-text mj-class="text-body03">
${strings("notification.observation.rescheduled.email.body.2", originalStartDateString, originalEndDateString)}
</mj-text>
<mj-text mj-class="text-body03">
${strings("notification.observation.rescheduled.email.body.3", newStartDateString, newEndDateString)}
</mj-text>
<mj-include path="../../manageSettings.ftlh.mjml"/>
</mj-column>
</mj-section>

<mj-include path="../../footer.ftlh.mjml"/>
</mj-body>
</mjml>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<#-- @ftlvariable name="" type="com.terraformation.backend.email.model.ObservationRescheduled" -->
${strings("notification.observation.rescheduled.email.body.1", plantingSiteName)}

${strings("notification.observation.rescheduled.email.body.2", originalStartDateString, originalEndDateString)}

${strings("notification.observation.rescheduled.email.body.3", newStartDateString, newEndDateString)}

------------------------------

${strings("notification.email.text.footer", manageSettingsUrl)}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<#-- @ftlvariable name="" type="com.terraformation.backend.email.model.ObservationRescheduled" -->
${strings("notification.observation.rescheduled.email.subject", organizationName, plantingSiteName)}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- <#-- @ftlvariable name="" type="com.terraformation.backend.email.model.ObservationScheduled" --> -->
<!-- <#setting date_format="full"> -->
<mjml>
<mj-include path="../../head.ftlh.mjml"/>

<mj-body>
<mj-include path="../../logo.ftlh.mjml"/>

<mj-section padding="0px">
<mj-column mj-class="body-wrapper">
<mj-text mj-class="text-body03-leading">
${strings("notification.observation.scheduled.email.body.1", plantingSiteName, startDateString, endDateString)}
</mj-text>
<mj-include path="../../manageSettings.ftlh.mjml"/>
</mj-column>
</mj-section>

<mj-include path="../../footer.ftlh.mjml"/>
</mj-body>
</mjml>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<#-- @ftlvariable name="" type="com.terraformation.backend.email.model.ObservationScheduled" -->
${strings("notification.observation.scheduled.email.body.1", plantingSiteName, startDateString, endDateString)}

------------------------------

${strings("notification.email.text.footer", manageSettingsUrl)}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<#-- @ftlvariable name="" type="com.terraformation.backend.email.model.ObservationScheduled" -->
${strings("notification.observation.scheduled.email.subject", organizationName, plantingSiteName)}
Loading

0 comments on commit d62b389

Please sign in to comment.