diff --git a/app/controllers/AccountController.scala b/app/controllers/AccountController.scala index f87092f7..0d776313 100644 --- a/app/controllers/AccountController.scala +++ b/app/controllers/AccountController.scala @@ -128,7 +128,7 @@ class AccountController( def validateEmail() = Action.async(parse.json) { implicit request => for { token <- request.parseBody[String](JsPath \ "token") - user <- accessesOrchestrator.validateDGCCRFEmail(token) + user <- accessesOrchestrator.validateAgentEmail(token) cookie <- authenticator.init(user.email) match { case Right(value) => Future.successful(value) case Left(error) => Future.failed(error) diff --git a/app/controllers/ReportController.scala b/app/controllers/ReportController.scala index 7735315e..04b779c8 100644 --- a/app/controllers/ReportController.scala +++ b/app/controllers/ReportController.scala @@ -199,7 +199,7 @@ class ReportController( reportWithDataOrchestrator .getReportFull(reportId, request.identity) .flatMap(_.liftTo[Future](AppError.ReportNotFound(reportId))) - .map(reportData => massImportService.reportSummaryWithAttachmentsZip(reportData)) + .flatMap(reportData => massImportService.reportSummaryWithAttachmentsZip(reportData)) .map(pdfSource => Ok.chunked( content = pdfSource, diff --git a/app/controllers/error/AppError.scala b/app/controllers/error/AppError.scala index 116ae3bb..6ec4924a 100644 --- a/app/controllers/error/AppError.scala +++ b/app/controllers/error/AppError.scala @@ -36,11 +36,11 @@ object AppError { override val titleForLogs: String = "server_error" } - final case class DGCCRFActivationTokenNotFound(token: String) extends NotFoundError { + final case class AgentActivationTokenNotFound(token: String) extends NotFoundError { override val `type`: String = "SC-0002" - override val title: String = s"DGCCRF user token $token not found" + override val title: String = s"Agent user token $token not found" override val details: String = s"Le lien d'activation n'est pas valide ($token). Merci de contacter le support" - override val titleForLogs: String = "dgccrf_activation_token_not_found" + override val titleForLogs: String = "agent_activation_token_not_found" } final case class CompanyActivationTokenNotFound(token: String, siret: SIRET) extends NotFoundError { diff --git a/app/orchestrators/AccessesOrchestrator.scala b/app/orchestrators/AccessesOrchestrator.scala index 570f91d6..764078ac 100644 --- a/app/orchestrators/AccessesOrchestrator.scala +++ b/app/orchestrators/AccessesOrchestrator.scala @@ -182,7 +182,7 @@ class AccessesOrchestrator( .findToken(token) accessToken <- maybeAccessToken .map(Future.successful(_)) - .getOrElse(Future.failed[AccessToken](DGCCRFActivationTokenNotFound(token))) + .getOrElse(Future.failed[AccessToken](AgentActivationTokenNotFound(token))) _ = logger.debug("Validating email") emailTo <- accessToken.emailedTo @@ -325,12 +325,12 @@ class AccessesOrchestrator( ) } yield logger.debug(s"Sent email validation to ${user.email}") - def validateDGCCRFEmail(token: String): Future[User] = + def validateAgentEmail(token: String): Future[User] = for { accessToken <- accessTokenRepository.findToken(token) emailValidationToken <- accessToken .filter(_.kind == ValidateEmail) - .liftTo[Future](DGCCRFActivationTokenNotFound(token)) + .liftTo[Future](AgentActivationTokenNotFound(token)) emailTo <- emailValidationToken.emailedTo.liftTo[Future]( ServerError("ValidateEmailToken should have valid email associated") @@ -350,10 +350,10 @@ class AccessesOrchestrator( user <- userOrchestrator .findOrError(email) .ensure { - logger.error("Cannot revalidate user with role different from DGCCRF") + logger.error("Cannot revalidate user with role different from DGCCRF or DGAL") CantPerformAction - }(_.userRole == UserRole.DGCCRF) - _ = logger.debug(s"Validating DGCCRF user email") + }(user => user.userRole == UserRole.DGCCRF || user.userRole == UserRole.DGAL) + _ = logger.debug(s"Validating agent user email") _ <- if (emailValidationToken.nonEmpty) { emailValidationToken.map(accessTokenRepository.validateEmail(_, user)).sequence diff --git a/app/orchestrators/AuthOrchestrator.scala b/app/orchestrators/AuthOrchestrator.scala index 9bed8a16..5608803e 100644 --- a/app/orchestrators/AuthOrchestrator.scala +++ b/app/orchestrators/AuthOrchestrator.scala @@ -92,7 +92,7 @@ class AuthOrchestrator( _ = logger.debug(s"Found user (maybe deleted)") _ <- handleDeletedUser(user, userLogin) _ = logger.debug(s"Check last validation email for DGCCRF users") - _ <- validateDGCCRFAccountLastEmailValidation(user) + _ <- validateAgentAccountLastEmailValidation(user) _ = logger.debug(s"Successful login for user") cookie <- getCookie(userLogin)(request) _ = logger.debug(s"Successful generated token for user") @@ -180,8 +180,8 @@ class AuthOrchestrator( } } yield cookie - private def validateDGCCRFAccountLastEmailValidation(user: User): Future[User] = user.userRole match { - case UserRole.DGCCRF if needsEmailRevalidation(user) => + private def validateAgentAccountLastEmailValidation(user: User): Future[User] = user.userRole match { + case UserRole.DGCCRF | UserRole.DGAL if needsEmailRevalidation(user) => accessesOrchestrator .sendEmailValidation(user) .flatMap(_ => throw DGCCRFUserEmailValidationExpired(user.email.value)) diff --git a/app/orchestrators/ReportFileOrchestrator.scala b/app/orchestrators/ReportFileOrchestrator.scala index 943acaee..9ac45e40 100644 --- a/app/orchestrators/ReportFileOrchestrator.scala +++ b/app/orchestrators/ReportFileOrchestrator.scala @@ -115,11 +115,11 @@ class ReportFileOrchestrator( for { reportFiles <- reportFileRepository .retrieveReportFiles(report.id) - filteredFilesByOrigin = reportFiles.filter { f => origin.contains(f.origin) || origin.isEmpty } - _ <- Future.successful(filteredFilesByOrigin).ensure(AppError.NoReportFiles)(_.nonEmpty) - } yield reportZipExportService.reportAttachmentsZip(report.creationDate, filteredFilesByOrigin) + _ <- Future.successful(filteredFilesByOrigin).ensure(AppError.NoReportFiles)(_.nonEmpty) + res <- reportZipExportService.reportAttachmentsZip(report.creationDate, filteredFilesByOrigin) + } yield res } diff --git a/app/orchestrators/ReportZipExportService.scala b/app/orchestrators/ReportZipExportService.scala index 87a838e1..580efa8f 100644 --- a/app/orchestrators/ReportZipExportService.scala +++ b/app/orchestrators/ReportZipExportService.scala @@ -5,6 +5,7 @@ import akka.stream.IOResult import akka.stream.Materializer import akka.stream.scaladsl.Source import akka.util.ByteString +import cats.implicits.toTraverseOps import controllers.HtmlFromTemplateGenerator import models.report.ReportFile import play.api.Logger @@ -39,29 +40,39 @@ class ReportZipExportService( def reportSummaryWithAttachmentsZip( reportWithData: ReportWithData - ): Source[ByteString, Future[IOResult]] = { + ): Future[Source[ByteString, Future[IOResult]]] = for { + reportAttachmentSources <- buildReportAttachmentsSources(reportWithData.report.creationDate, reportWithData.files) + reportPdfSummarySource = buildReportPdfSummarySource(reportWithData) + fileSourcesFutures = reportAttachmentSources :+ reportPdfSummarySource + } yield ZipBuilder.buildZip(fileSourcesFutures) - val reportAttachmentSources = reportWithData.files.zipWithIndex.map { case (file, i) => - buildReportAttachmentSource(reportWithData.report.creationDate, file, i) + private def buildReportAttachmentsSources( + creationDate: OffsetDateTime, + reportFiles: Seq[ReportFile] + ) = for { + existingFiles <- reportFiles.traverse(f => + s3Service.exists(f.storageFilename).map(exists => (f, exists)) + ) map (_.collect { case (file, true) => + file + }) + reportAttachmentSources = existingFiles.zipWithIndex.map { case (file, i) => + buildReportAttachmentSource(creationDate, file, i + 1) } - val reportPdfSummarySource = buildReportPdfSummarySource(reportWithData) - - val fileSourcesFutures = reportAttachmentSources :+ reportPdfSummarySource - - ZipBuilder.buildZip(fileSourcesFutures) - } + } yield reportAttachmentSources def reportAttachmentsZip( creationDate: OffsetDateTime, - reports: Seq[ReportFile] - ): Source[ByteString, Future[IOResult]] = { - - val reportAttachmentSources = reports.zipWithIndex.map { case (file, i) => + reportFiles: Seq[ReportFile] + ): Future[Source[ByteString, Future[IOResult]]] = for { + existingFiles <- reportFiles.traverse(f => + s3Service.exists(f.storageFilename).map(exists => (f, exists)) + ) map (_.collect { case (file, true) => + file + }) + reportAttachmentSources = existingFiles.zipWithIndex.map { case (file, i) => buildReportAttachmentSource(creationDate, file, i + 1) } - - ZipBuilder.buildZip(reportAttachmentSources) - } + } yield ZipBuilder.buildZip(reportAttachmentSources) private def buildReportPdfSummarySource( reportWithData: ReportWithData diff --git a/app/repositories/user/UserRepository.scala b/app/repositories/user/UserRepository.scala index 533bcfe9..d62f26cd 100644 --- a/app/repositories/user/UserRepository.scala +++ b/app/repositories/user/UserRepository.scala @@ -2,6 +2,7 @@ package repositories.user import authentication.PasswordHasherRegistry import controllers.error.AppError.EmailAlreadyExist +import models.UserRole.DGAL import models.UserRole.DGCCRF import models._ import play.api.Logger @@ -36,29 +37,29 @@ class UserRepository( import dbConfig._ - override def listExpiredDGCCRF(expirationDate: OffsetDateTime): Future[List[User]] = + override def listExpiredAgents(expirationDate: OffsetDateTime): Future[List[User]] = db .run( table - .filter(_.role === DGCCRF.entryName) + .filter(user => user.role === DGCCRF.entryName || user.role === DGAL.entryName) .filter(_.lastEmailValidation <= expirationDate) .to[List] .result ) - override def listInactiveDGCCRFWithSentEmailCount( + override def listInactiveAgentsWithSentEmailCount( reminderDate: OffsetDateTime, expirationDate: OffsetDateTime ): Future[List[(User, Option[Int])]] = db.run( UserTable.table - .filter(_.role === DGCCRF.entryName) + .filter(user => user.role === DGCCRF.entryName || user.role === DGAL.entryName) .filter(_.lastEmailValidation <= reminderDate) .filter(_.lastEmailValidation > expirationDate) .joinLeft( EventTable.table - .filter(_.action === ActionEvent.EMAIL_INACTIVE_DGCCRF_ACCOUNT.value) - .filter(_.eventType === EventType.DGCCRF.value) + .filter(_.action === ActionEvent.EMAIL_INACTIVE_AGENT_ACCOUNT.value) + .filter(user => user.eventType === EventType.DGCCRF.value || user.eventType === EventType.DGAL.value) .filter(_.userId.isDefined) .groupBy(_.userId) .map { case (userId, results) => userId -> results.length } diff --git a/app/repositories/user/UserRepositoryInterface.scala b/app/repositories/user/UserRepositoryInterface.scala index 5f2fa2a0..a1ba04ed 100644 --- a/app/repositories/user/UserRepositoryInterface.scala +++ b/app/repositories/user/UserRepositoryInterface.scala @@ -11,9 +11,9 @@ import scala.concurrent.Future trait UserRepositoryInterface extends CRUDRepositoryInterface[User] { - def listExpiredDGCCRF(expirationDate: OffsetDateTime): Future[List[User]] + def listExpiredAgents(expirationDate: OffsetDateTime): Future[List[User]] - def listInactiveDGCCRFWithSentEmailCount( + def listInactiveAgentsWithSentEmailCount( reminderDate: OffsetDateTime, expirationDate: OffsetDateTime ): Future[List[(User, Option[Int])]] diff --git a/app/services/S3Service.scala b/app/services/S3Service.scala index b54835ee..9fa6513d 100644 --- a/app/services/S3Service.scala +++ b/app/services/S3Service.scala @@ -62,6 +62,9 @@ class S3Service(implicit alpakkaS3Client .getObject(bucketName, bucketKey) + def exists(bucketKey: String): Future[Boolean] = + S3.getObjectMetadata(bucketName, bucketKey).runWith(Sink.headOption).map(_.isDefined) + override def delete(bucketKey: String): Future[Done] = alpakkaS3Client.deleteObject(bucketName, bucketKey).runWith(Sink.head) diff --git a/app/services/S3ServiceInterface.scala b/app/services/S3ServiceInterface.scala index 5bb1f537..f59d2b71 100644 --- a/app/services/S3ServiceInterface.scala +++ b/app/services/S3ServiceInterface.scala @@ -22,4 +22,5 @@ trait S3ServiceInterface { def getSignedUrl(bucketKey: String, method: HttpMethod = HttpMethod.GET): String def downloadFromBucket(bucketKey: String): Source[ByteString, Future[ObjectMetadata]] + def exists(bucketKey: String): Future[Boolean] } diff --git a/app/tasks/account/InactiveDgccrfAccountReminderTask.scala b/app/tasks/account/InactiveDgccrfAccountReminderTask.scala index c608cdc2..19c0c6c2 100644 --- a/app/tasks/account/InactiveDgccrfAccountReminderTask.scala +++ b/app/tasks/account/InactiveDgccrfAccountReminderTask.scala @@ -1,6 +1,7 @@ package tasks.account import models.User +import models.UserRole import models.event.Event import play.api.Logger import play.api.libs.json.Json @@ -35,11 +36,11 @@ class InactiveDgccrfAccountReminderTask( inactivePeriod: Period ): Future[Unit] = for { - firstReminderEvents <- userRepository.listInactiveDGCCRFWithSentEmailCount( + firstReminderEvents <- userRepository.listInactiveAgentsWithSentEmailCount( firstReminderThreshold, expirationDateThreshold ) - secondReminderEvents <- userRepository.listInactiveDGCCRFWithSentEmailCount( + secondReminderEvents <- userRepository.listInactiveAgentsWithSentEmailCount( secondReminderThreshold, expirationDateThreshold ) @@ -64,8 +65,8 @@ class InactiveDgccrfAccountReminderTask( None, Some(user.id), now, - EventType.DGCCRF, - ActionEvent.EMAIL_INACTIVE_DGCCRF_ACCOUNT, + if (user.userRole == UserRole.DGAL) EventType.DGAL else EventType.DGCCRF, + ActionEvent.EMAIL_INACTIVE_AGENT_ACCOUNT, Json.obj( "lastEmailValidation" -> user.lastEmailValidation, "expirationDate" -> expirationDate diff --git a/app/tasks/account/InactiveDgccrfAccountRemoveTask.scala b/app/tasks/account/InactiveDgccrfAccountRemoveTask.scala index e3f77abf..eb8aa6d9 100644 --- a/app/tasks/account/InactiveDgccrfAccountRemoveTask.scala +++ b/app/tasks/account/InactiveDgccrfAccountRemoveTask.scala @@ -28,7 +28,7 @@ class InactiveDgccrfAccountRemoveTask( logger.info(s"Soft delete inactive DGCCRF accounts with last validation below $expirationDateThreshold") for { - inactiveDGCCRFAccounts <- userRepository.listExpiredDGCCRF(expirationDateThreshold) + inactiveDGCCRFAccounts <- userRepository.listExpiredAgents(expirationDateThreshold) results <- inactiveDGCCRFAccounts.map(removeWithSubscriptions).sequence } yield results.sequence } diff --git a/app/utils/Constants.scala b/app/utils/Constants.scala index 73cd15df..647d19e5 100644 --- a/app/utils/Constants.scala +++ b/app/utils/Constants.scala @@ -112,7 +112,7 @@ object Constants { object USER_DELETION extends ActionEventValue("Suppression d'un utilisateur") - object EMAIL_INACTIVE_DGCCRF_ACCOUNT extends ActionEventValue("Email «compte inactif» envoyé à l'agent") + object EMAIL_INACTIVE_AGENT_ACCOUNT extends ActionEventValue("Email «compte inactif» envoyé à l'agent") object CONSUMER_THREATEN_BY_PRO extends ActionEventValue("ConsumerThreatenByProReportDeletion") object REFUND_BLACKMAIL extends ActionEventValue("RefundBlackMailReportDeletion") diff --git a/test/models/UserRoleTest.scala b/test/models/UserRoleTest.scala index 4cd28573..cba3cea9 100644 --- a/test/models/UserRoleTest.scala +++ b/test/models/UserRoleTest.scala @@ -12,6 +12,7 @@ class UserRoleTest extends Specification { "get value from string name" in { UserRole.withName("DGCCRF") shouldEqual UserRole.DGCCRF + UserRole.withName("DGAL") shouldEqual UserRole.DGAL UserRole.withName("Admin") shouldEqual UserRole.Admin UserRole.withName("Professionnel") shouldEqual UserRole.Professionnel Try(UserRole.withName("XXXXXXXXXX")).isFailure shouldEqual true diff --git a/test/orchestrators/AccessesOrchestratorTest.scala b/test/orchestrators/AccessesOrchestratorTest.scala index 8cf89d0f..5d6bf677 100644 --- a/test/orchestrators/AccessesOrchestratorTest.scala +++ b/test/orchestrators/AccessesOrchestratorTest.scala @@ -95,7 +95,7 @@ class AccessesOrchestratorTest extends Specification with AppSpec { "email validation should fail when token not found" >> { val unknownToken = "" components.accessesOrchestrator - .validateDGCCRFEmail(unknownToken) must throwA[DGCCRFActivationTokenNotFound].await + .validateAgentEmail(unknownToken) must throwA[AgentActivationTokenNotFound].await } "email validation should fail when no validation email token found" >> { @@ -109,11 +109,11 @@ class AccessesOrchestratorTest extends Specification with AppSpec { ) val result = for { _ <- components.accessTokenRepository.create(nonRelevantToken) - res <- components.accessesOrchestrator.validateDGCCRFEmail(nonRelevantToken.token) + res <- components.accessesOrchestrator.validateAgentEmail(nonRelevantToken.token) } yield res - result must throwA[DGCCRFActivationTokenNotFound].await + result must throwA[AgentActivationTokenNotFound].await } "email validation should fail when validation email token found not link to any email" >> { @@ -128,7 +128,7 @@ class AccessesOrchestratorTest extends Specification with AppSpec { val result = for { _ <- components.accessTokenRepository.create(invalidValidationEmailToken) - res <- components.accessesOrchestrator.validateDGCCRFEmail(invalidValidationEmailToken.token) + res <- components.accessesOrchestrator.validateAgentEmail(invalidValidationEmailToken.token) } yield res @@ -147,7 +147,7 @@ class AccessesOrchestratorTest extends Specification with AppSpec { val result = for { _ <- components.accessTokenRepository.create(validationEmailToken) - res <- components.accessesOrchestrator.validateDGCCRFEmail(validationEmailToken.token) + res <- components.accessesOrchestrator.validateAgentEmail(validationEmailToken.token) } yield res @@ -170,7 +170,7 @@ class AccessesOrchestratorTest extends Specification with AppSpec { val result = for { _ <- components.accessTokenRepository.create(validationEmailToken) _ <- components.userRepository.create(dgccrfUser) - res <- components.accessesOrchestrator.validateDGCCRFEmail(validationEmailToken.token) + res <- components.accessesOrchestrator.validateAgentEmail(validationEmailToken.token) } yield res diff --git a/test/repositories/UserRepositorySpec.scala b/test/repositories/UserRepositorySpec.scala index d36b5c84..033cde3a 100644 --- a/test/repositories/UserRepositorySpec.scala +++ b/test/repositories/UserRepositorySpec.scala @@ -64,7 +64,7 @@ class UserRepositorySpec(implicit ee: ExecutionEnv) extends Specification with A userId = Some(inactiveDgccrfUserWithEmails.id), creationDate = now, eventType = EventType.DGCCRF, - action = ActionEvent.EMAIL_INACTIVE_DGCCRF_ACCOUNT, + action = ActionEvent.EMAIL_INACTIVE_AGENT_ACCOUNT, details = Json.obj() ) ), @@ -79,7 +79,7 @@ class UserRepositorySpec(implicit ee: ExecutionEnv) extends Specification with A userId = Some(inactiveDgccrfUserWithEmails.id), creationDate = now, eventType = EventType.DGCCRF, - action = ActionEvent.EMAIL_INACTIVE_DGCCRF_ACCOUNT, + action = ActionEvent.EMAIL_INACTIVE_AGENT_ACCOUNT, details = Json.obj() ) ), @@ -113,12 +113,12 @@ class UserRepositorySpec(implicit ee: ExecutionEnv) extends Specification with A def e4 = userRepository.get(userToto.id).map(_.isDefined) must beFalse.await def e6 = userRepository - .listInactiveDGCCRFWithSentEmailCount(now.minusMonths(1), now.minusYears(1)) + .listInactiveAgentsWithSentEmailCount(now.minusMonths(1), now.minusYears(1)) .map(_.map { case (user, count) => (user.id, count) }) must beEqualTo( List(inactiveDgccrfUser.id -> None, inactiveDgccrfUserWithEmails.id -> Some(2)) ).await def e7 = userRepository - .listInactiveDGCCRFWithSentEmailCount(now.minusMonths(1), now.minusMonths(2)) must beEmpty[List[ + .listInactiveAgentsWithSentEmailCount(now.minusMonths(1), now.minusMonths(2)) must beEmpty[List[ (User, Option[Int]) ]].await diff --git a/test/tasks/account/InactiveDgccrfAccountReminderTaskSpec.scala b/test/tasks/account/InactiveDgccrfAccountReminderTaskSpec.scala index 0a360f42..96fda97b 100644 --- a/test/tasks/account/InactiveDgccrfAccountReminderTaskSpec.scala +++ b/test/tasks/account/InactiveDgccrfAccountReminderTaskSpec.scala @@ -82,21 +82,21 @@ class InactiveDgccrfAccountReminderTaskSpec(implicit ee: ExecutionEnv) Some(firstMailToSendUser.id), OffsetDateTime.now(), EventType.DGCCRF, - ActionEvent.EMAIL_INACTIVE_DGCCRF_ACCOUNT, + ActionEvent.EMAIL_INACTIVE_AGENT_ACCOUNT, Json.obj() ) - when(mockUserRepository.listInactiveDGCCRFWithSentEmailCount(firstReminder, expiration)) + when(mockUserRepository.listInactiveAgentsWithSentEmailCount(firstReminder, expiration)) .thenReturn(Future.successful(List(firstMailToSendUser -> None))) - when(mockUserRepository.listInactiveDGCCRFWithSentEmailCount(secondReminder, expiration)) + when(mockUserRepository.listInactiveAgentsWithSentEmailCount(secondReminder, expiration)) .thenReturn(Future.successful(List.empty)) when(mockEventRepository.create(any[Event]())).thenReturn(Future.successful(event)) val test = new InactiveDgccrfAccountReminderTask(mockUserRepository, mockEventRepository, mailService) test.sendReminderEmail(firstReminder, secondReminder, expiration, Period.ofYears(1)) must beEqualTo(()).await - there was one(mockUserRepository).listInactiveDGCCRFWithSentEmailCount(firstReminder, expiration) - there was one(mockUserRepository).listInactiveDGCCRFWithSentEmailCount(secondReminder, expiration) + there was one(mockUserRepository).listInactiveAgentsWithSentEmailCount(firstReminder, expiration) + there was one(mockUserRepository).listInactiveAgentsWithSentEmailCount(secondReminder, expiration) there was one(mockEventRepository).create(any[Event]()) there was one(mockMailRetriesService).sendEmailWithRetries(any[EmailRequest]()) } diff --git a/test/utils/S3ServiceMock.scala b/test/utils/S3ServiceMock.scala index 1e6a1203..e52274b6 100644 --- a/test/utils/S3ServiceMock.scala +++ b/test/utils/S3ServiceMock.scala @@ -25,4 +25,6 @@ class S3ServiceMock extends S3ServiceInterface { override def getSignedUrl(bucketKey: String, method: HttpMethod): String = ??? override def downloadFromBucket(bucketKey: String): Source[ByteString, Future[ObjectMetadata]] = ??? + + override def exists(bucketKey: String): Future[Boolean] = ??? }