Skip to content

Commit

Permalink
[TRELLO-2008] WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
charlescd committed Sep 30, 2023
1 parent 9238b4c commit 20a8696
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 46 deletions.
5 changes: 2 additions & 3 deletions app/loader/SignalConsoApplicationLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -453,13 +453,12 @@ class SignalConsoComponents(
messagesApi
)
val reportReminderTask = new ReportRemindersTask(
actorSystem,
reportRepository,
eventRepository,
mailService,
companiesVisibilityOrchestrator,
taskConfiguration
companiesVisibilityOrchestrator
)
reportReminderTask.schedule(actorSystem, taskConfiguration)

def companySyncService: CompanySyncServiceInterface = new CompanySyncService(
applicationConfiguration.task.companyUpdate
Expand Down
114 changes: 71 additions & 43 deletions app/tasks/report/ReportRemindersTask.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import repositories.event.EventRepositoryInterface
import repositories.report.ReportRepositoryInterface
import services.Email.ProReportsReadReminder
import services.Email.ProReportsUnreadReminder
import services.MailService
import services.Email
import services.MailServiceInterface
import tasks.getTodayAtStartOfDayParis
import tasks.scheduleTask
import utils.Constants.ActionEvent._
import utils.Constants.EventType.SYSTEM
import utils.EmailAddress

import java.time._
import java.util.UUID
import scala.concurrent.ExecutionContext
Expand All @@ -26,18 +29,14 @@ import scala.util.Failure
import scala.util.Success
import utils.Logs.RichLogger
class ReportRemindersTask(
actorSystem: ActorSystem,
reportRepository: ReportRepositoryInterface,
eventRepository: EventRepositoryInterface,
mailService: MailService,
companiesVisibilityOrchestrator: CompaniesVisibilityOrchestrator,
taskConfiguration: TaskConfiguration
mailService: MailServiceInterface,
companiesVisibilityOrchestrator: CompaniesVisibilityOrchestrator
)(implicit val executionContext: ExecutionContext) {

val logger: Logger = Logger(this.getClass)

val conf = taskConfiguration.reportReminders

// In practice, since we require 7 full days between the previous email and the next one,
// the email will fire at J+8
// Typically (if the pro account existed when the report was created):
Expand All @@ -48,15 +47,25 @@ class ReportRemindersTask(
val delayBetweenReminderEmails: Period = Period.ofDays(7)
val maxReminderCount = 2

scheduleTask(
actorSystem,
taskConfiguration,
startTime = conf.startTime,
interval = conf.intervalInHours,
taskName = "report_reminders_task"
)(runTask(taskRunDate = getTodayAtStartOfDayParis()))
def schedule(actorSystem: ActorSystem, taskConfiguration: TaskConfiguration): Unit = {
val conf = taskConfiguration.reportReminders

def runTask(taskRunDate: OffsetDateTime): Future[Unit] = {
scheduleTask(
actorSystem,
taskConfiguration,
startTime = conf.startTime,
interval = conf.intervalInHours,
taskName = "report_reminders_task"
)(runTask(taskRunDate = getTodayAtStartOfDayParis()).map { case (failures, successes) =>
logger.info(
s"Successfully sent ${successes.length} reminder emails sent for ${successes.map(_.length).sum} reports"
)
if (failures.nonEmpty)
logger.error(s"Failed to send ${failures.length} reminder emails for ${failures.map(_.length).sum} reports")
})
}

def runTask(taskRunDate: OffsetDateTime): Future[(List[List[UUID]], List[List[UUID]])] = {
val ongoingReportsStatus = List(ReportStatus.TraitementEnCours, ReportStatus.Transmis)
for {
ongoingReportsWithUsers <- getReportsByStatusWithUsers(ongoingReportsStatus)
Expand All @@ -67,11 +76,13 @@ class ReportRemindersTask(
shouldSendReminderEmail(report, taskRunDate, eventsByReportId)
}
_ = logger.info(s"Found ${finalReportsWithUsers.size} reports for which we should send a reminder")
_ <- sendReminderEmailsWithErrorHandling(finalReportsWithUsers)
} yield ()
result <- sendReminderEmailsWithErrorHandling(finalReportsWithUsers)
} yield result
}

private def sendReminderEmailsWithErrorHandling(reportsWithUsers: List[(Report, List[User])]): Future[Unit] = {
private def sendReminderEmailsWithErrorHandling(
reportsWithUsers: List[(Report, List[User])]
): Future[(List[List[UUID]], List[List[UUID]])] = {
logger.info(s"Sending reminders for ${reportsWithUsers.length} reports")
val reportsPerUsers = reportsWithUsers.groupBy(_._2).view.mapValues(_.map(_._1))
val reportsPerCompanyPerUsers = reportsPerUsers.mapValues(_.groupBy(_.companyId)).mapValues(_.values).toMap
Expand All @@ -80,24 +91,41 @@ class ReportRemindersTask(
successesOrFailuresList <- Future.sequence(reportsPerCompanyPerUsers.toList.flatMap {
case (users, reportsPerCompany) =>
reportsPerCompany.map { reports =>
logger.infoWithTitle("report_reminders_task_item", s"Closed reports ${reports.map(_.id)}")
sendReminderEmail(reports, users).transform {
case Success(_) => Success(Right(reports.map(_.id)))
case Failure(err) =>
logger.errorWithTitle(
"report_reminders_task_item_error",
s"Error sending reminder email for reports ${reports.map(_.id)} to ${users.length} users",
err
)
Success(Left(reports.map(_.id)))
}
}
val (readByPros, notReadByPros) = reports.partition(_.isReadByPro)

for {
readByProsSent <-
if (readByPros.nonEmpty) test(readByPros, users, ProReportsReadReminder, EMAIL_PRO_REMIND_NO_ACTION)
else Future.successful(Right(List.empty))
notReadByProsSent <-
if (notReadByPros.nonEmpty)
test(notReadByPros, users, ProReportsUnreadReminder, EMAIL_PRO_REMIND_NO_READING)
else Future.successful(Right(List.empty))
} yield List(readByProsSent, notReadByProsSent)

}
})
(failures, successes) = successesOrFailuresList.partitionMap(identity)
_ = logger.info(s"Successful reminder emails sent for ${successes.length} reports")
_ = if (failures.nonEmpty) logger.error(s"Failed to send reminder emails for ${failures.length} reports")
} yield ()
(failures, successes) = successesOrFailuresList.flatten.partitionMap(identity)
} yield (failures.filter(_.nonEmpty), successes.filter(_.nonEmpty))
}

private def test(
reports: List[Report],
users: List[User],
email: (List[EmailAddress], List[Report], Period) => Email,
action: ActionEventValue
) = {
logger.infoWithTitle("report_reminders_task_item", s"Sending reports ${reports.map(_.id)}")
sendReminderEmail(reports, users, email, action).transform {
case Success(_) => Success(Right(reports.map(_.id)))
case Failure(err) =>
logger.errorWithTitle(
"report_reminders_task_item_error",
s"Error sending reminder email for reports ${reports.map(_.id)} to ${users.length} users",
err
)
Success(Left(reports.map(_.id)))
}
}

private def shouldSendReminderEmail(
Expand All @@ -121,17 +149,17 @@ class ReportRemindersTask(

private def sendReminderEmail(
reports: List[Report],
users: List[User]
users: List[User],
email: (List[EmailAddress], List[Report], Period) => Email,
action: ActionEventValue
): Future[Unit] = {
val emailAddresses = users.map(_.email)
val (readByPros, notReadByPros) = reports.partition(_.isReadByPro)

logger.debug(s"Sending reminder email")
for {
_ <- mailService.send(ProReportsReadReminder(emailAddresses, readByPros, delayBetweenReminderEmails))
_ <- mailService.send(ProReportsUnreadReminder(emailAddresses, notReadByPros, delayBetweenReminderEmails))
_ <- mailService.send(email(emailAddresses, reports, delayBetweenReminderEmails))
_ <- Future.sequence(
readByPros.map { report =>
reports.map { report =>
eventRepository.create(
Event(
UUID.randomUUID(),
Expand All @@ -140,14 +168,14 @@ class ReportRemindersTask(
None,
OffsetDateTime.now(),
SYSTEM,
EMAIL_PRO_REMIND_NO_ACTION,
action,
stringToDetailsJsValue(s"Relance envoyée à ${emailAddresses.mkString(", ")}")
)
)
}
)
_ <- Future.sequence(
notReadByPros.map { report =>
reports.map { report =>
eventRepository.create(
Event(
UUID.randomUUID(),
Expand All @@ -156,7 +184,7 @@ class ReportRemindersTask(
None,
OffsetDateTime.now(),
SYSTEM,
EMAIL_PRO_REMIND_NO_READING,
action,
stringToDetailsJsValue(s"Relance envoyée à ${emailAddresses.mkString(", ")}")
)
)
Expand All @@ -174,7 +202,7 @@ class ReportRemindersTask(
id <- r.companyId
} yield (siret, id)
)
usersByCompanyId <- companiesVisibilityOrchestrator.fetchUsersWithHeadOffices(companiesSiretsAndIds)
usersByCompanyId <- companiesVisibilityOrchestrator.fetchUsersWithHeadOffices(companiesSiretsAndIds.distinct)
} yield reports.flatMap(r => r.companyId.map(companyId => (r, usersByCompanyId.getOrElse(companyId, Nil))))

}
108 changes: 108 additions & 0 deletions test/tasks/report/ReportRemindersTaskUnitSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package tasks.report

import models.company.AccessLevel
import models.event.Event
import models.report.ReportStatus
import orchestrators.CompaniesVisibilityOrchestrator
import org.mockito.Mockito.when
import org.mockito.ArgumentMatchers.argThat
import org.mockito.ArgumentMatchers.{eq => eqTo}
import org.specs2.matcher.FutureMatchers
import org.specs2.mock.Mockito.any
import org.specs2.mock.Mockito.mock
import org.specs2.mutable.Specification
import repositories.company.CompanyRepositoryInterface
import repositories.companyaccess.CompanyAccessRepositoryInterface
import repositories.event.EventRepositoryInterface
import repositories.report.ReportRepositoryInterface
import services.Email
import services.MailServiceInterface
import utils.Constants.ActionEvent.EMAIL_PRO_NEW_REPORT
import utils.Constants.ActionEvent.EMAIL_PRO_REMIND_NO_READING
import utils.Constants.EventType
import utils.Fixtures
import utils.SIREN

import java.time.OffsetDateTime
import java.time.Period
import scala.concurrent.Await
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt

class ReportRemindersTaskUnitSpec extends Specification with FutureMatchers {

def argMatching[T](pf: PartialFunction[Any, Unit]) = argThat[T](pf.isDefinedAt(_))

"ReportReminderTask" should {
"successfully send grouped emails by company" in {
val reportRepository = mock[ReportRepositoryInterface]
val eventRepository = mock[EventRepositoryInterface]
val mailService = mock[MailServiceInterface]
val companyRepository = mock[CompanyRepositoryInterface]
val companyAccessRepository = mock[CompanyAccessRepositoryInterface]
val companiesVisibilityOrchestrator =
new CompaniesVisibilityOrchestrator(companyRepository, companyAccessRepository)

val reportRemindersTask =
new ReportRemindersTask(reportRepository, eventRepository, mailService, companiesVisibilityOrchestrator)

val company1 = Fixtures.genCompany.sample.get
val company2 = Fixtures.genCompany.sample.get
val report1 = Fixtures.genReportForCompany(company1).sample.get.copy(status = ReportStatus.TraitementEnCours)
val report2 = Fixtures.genReportForCompany(company1).sample.get.copy(status = ReportStatus.TraitementEnCours)
val report3 = Fixtures.genReportForCompany(company1).sample.get.copy(status = ReportStatus.Transmis)
val report4 = Fixtures.genReportForCompany(company2).sample.get.copy(status = ReportStatus.TraitementEnCours)
val report5 = Fixtures.genReportForCompany(company2).sample.get.copy(status = ReportStatus.Transmis)
val report6 = Fixtures.genReportForCompany(company2).sample.get.copy(status = ReportStatus.Transmis)
val report7 = Fixtures.genReportForCompany(company2).sample.get.copy(status = ReportStatus.Transmis)
val proUser1 = Fixtures.genProUser.sample.get

val event3 = Fixtures.genEventForReport(report3.id, eventType = EventType.SYSTEM, EMAIL_PRO_NEW_REPORT).sample.get
val event7 =
Fixtures.genEventForReport(report7.id, eventType = EventType.SYSTEM, EMAIL_PRO_REMIND_NO_READING).sample.get

when(reportRepository.getByStatus(List(ReportStatus.TraitementEnCours, ReportStatus.Transmis)))
.thenReturn(Future.successful(List(report1, report2, report3, report4, report5, report6, report7)))
when(
companyAccessRepository.fetchUsersByCompanyIds(eqTo(List(company1.id, company2.id)), any[Seq[AccessLevel]]())
).thenReturn(Future.successful(Map(company1.id -> List(proUser1), company2.id -> List(proUser1))))
when(
companyRepository.findHeadOffices(
List(SIREN.fromSIRET(company1.siret), SIREN.fromSIRET(company2.siret)),
openOnly = false
)
).thenReturn(Future.successful(List.empty))
when(companyAccessRepository.fetchUsersByCompanyIds(eqTo(List.empty), any[Seq[AccessLevel]]()))
.thenReturn(Future.successful(Map.empty))
when(eventRepository.fetchEventsOfReports(List(report1, report2, report3, report4, report5, report6, report7)))
.thenReturn(Future.successful(Map(report3.id -> List(event3), report7.id -> List(event7))))

when(
mailService.send(Email.ProReportsUnreadReminder(List(proUser1.email), List(report1, report2), Period.ofDays(7)))
).thenReturn(Future.unit)
when(mailService.send(Email.ProReportsUnreadReminder(List(proUser1.email), List(report4), Period.ofDays(7))))
.thenReturn(Future.unit)
when(
mailService.send(Email.ProReportsReadReminder(List(proUser1.email), List(report5, report6), Period.ofDays(7)))
).thenReturn(Future.unit)

when(eventRepository.create(argMatching[Event] { case Event(_, Some(report1.id), _, _, _, _, _, _) => }))
.thenReturn(Future.successful(null))
when(eventRepository.create(argMatching[Event] { case Event(_, Some(report2.id), _, _, _, _, _, _) => }))
.thenReturn(Future.successful(null))
when(eventRepository.create(argMatching[Event] { case Event(_, Some(report4.id), _, _, _, _, _, _) => }))
.thenReturn(Future.successful(null))
when(eventRepository.create(argMatching[Event] { case Event(_, Some(report5.id), _, _, _, _, _, _) => }))
.thenReturn(Future.successful(null))
when(eventRepository.create(argMatching[Event] { case Event(_, Some(report6.id), _, _, _, _, _, _) => }))
.thenReturn(Future.successful(null))

val futureRes = reportRemindersTask.runTask(OffsetDateTime.now())
val res = Await.result(futureRes, 1.second)

res._1.length shouldEqual 0 // 0 failures
res._2.length shouldEqual 3 // 3 emails sent
}
}
}

0 comments on commit 20a8696

Please sign in to comment.