diff --git a/app/controllers/ReportController.scala b/app/controllers/ReportController.scala
index 8b836c4a..d2239790 100644
--- a/app/controllers/ReportController.scala
+++ b/app/controllers/ReportController.scala
@@ -17,6 +17,7 @@ import play.api.i18n.MessagesProvider
import play.api.libs.json.JsValue
import play.api.libs.json.Json
import play.api.mvc.Action
+import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import repositories.company.CompanyRepositoryInterface
import repositories.report.ReportRepositoryInterface
@@ -56,6 +57,24 @@ class ReportController(
val logger: Logger = Logger(this.getClass)
+ def isReattributable(reportId: UUID): Action[AnyContent] = Act.public.standardLimit.async { _ =>
+ reportOrchestrator.isReattributable(reportId).map(Ok(_))
+ }
+
+ def reattribute(reportId: UUID): Action[JsValue] = Act.public.standardLimit.async(parse.json) { implicit request =>
+ implicit val userRole: Option[UserRole] = None
+ val consumerIp = ConsumerIp(request.remoteAddress)
+ for {
+ reattributeCompany <- request.parseBody[ReattributeCompany]()
+ createdReport <- reportOrchestrator.reattribute(
+ reportId,
+ reattributeCompany.company,
+ reattributeCompany.metadata,
+ consumerIp
+ )
+ } yield Ok(Json.toJson(createdReport))
+ }
+
def createReport: Action[JsValue] = Act.public.standardLimit.async(parse.json) { implicit request =>
implicit val userRole: Option[UserRole] = None
for {
diff --git a/app/controllers/error/AppError.scala b/app/controllers/error/AppError.scala
index 6fcef11d..7e1abd2c 100644
--- a/app/controllers/error/AppError.scala
+++ b/app/controllers/error/AppError.scala
@@ -656,4 +656,20 @@ object AppError {
override val titleForLogs: String = "website_not_identified"
}
+
+ final case class ReportNotReattributable(id: UUID) extends BadRequestError {
+ override val scErrorCode: String = "SC-0072"
+ override val title: String = s"Report is not reattributable"
+ override val details: String =
+ s"Le signalement $id n'est pas réattribuable"
+ override val titleForLogs: String = "report_not_reattributable"
+ }
+
+ final case object CantReattributeToTheSameCompany extends BadRequestError {
+ override val scErrorCode: String = "SC-0073"
+ override val title: String = s"Report is not reattributable"
+ override val details: String =
+ "Vous ne pouvez pas réattribuer ce signalement à la même entreprise"
+ override val titleForLogs: String = "report_not_reattributable"
+ }
}
diff --git a/app/models/ReattributeCompany.scala b/app/models/ReattributeCompany.scala
new file mode 100644
index 00000000..6a8c30be
--- /dev/null
+++ b/app/models/ReattributeCompany.scala
@@ -0,0 +1,15 @@
+package models
+
+import models.report.reportmetadata.ReportMetadataDraft
+import play.api.libs.json.Json
+import play.api.libs.json.Reads
+import tasks.company.CompanySearchResult
+
+case class ReattributeCompany(
+ company: CompanySearchResult,
+ metadata: ReportMetadataDraft
+)
+
+object ReattributeCompany {
+ implicit val reads: Reads[ReattributeCompany] = Json.reads[ReattributeCompany]
+}
diff --git a/app/orchestrators/ReportFileOrchestrator.scala b/app/orchestrators/ReportFileOrchestrator.scala
index b47533e8..2a257439 100644
--- a/app/orchestrators/ReportFileOrchestrator.scala
+++ b/app/orchestrators/ReportFileOrchestrator.scala
@@ -234,6 +234,23 @@ class ReportFileOrchestrator(
_ <- s3Service.delete(filename)
} yield res
+ def duplicate(fileId: ReportFileId, filename: String, maybeReportId: Option[UUID]): Future[ReportFile] = for {
+ file <- getFileByIdAndName(fileId, filename)
+ data <- s3Service.download(file.storageFilename)
+ newName = s"${UUID.randomUUID()}_$filename"
+ newFile = ReportFile(
+ id = ReportFileId.generateId(),
+ reportId = maybeReportId,
+ creationDate = OffsetDateTime.now(),
+ filename = filename,
+ storageFilename = newName,
+ origin = file.origin,
+ avOutput = file.avOutput
+ )
+ newReportFile <- reportFileRepository.create(newFile)
+ _ <- Source.single(data).runWith(s3Service.upload(newReportFile.storageFilename))
+ } yield newReportFile
+
private def checkIsNotYetUsedInReport(file: ReportFile): Try[Unit] =
file.reportId match {
case None => Success(())
diff --git a/app/orchestrators/ReportOrchestrator.scala b/app/orchestrators/ReportOrchestrator.scala
index 181bbea1..db283c08 100644
--- a/app/orchestrators/ReportOrchestrator.scala
+++ b/app/orchestrators/ReportOrchestrator.scala
@@ -25,6 +25,7 @@ import models.report.ReportStatus.hasResponse
import models.report.ReportWordOccurrence.StopWords
import models.report._
import models.report.reportmetadata.ReportExtra
+import models.report.reportmetadata.ReportMetadataDraft
import models.report.reportmetadata.ReportWithMetadataAndBookmark
import models.token.TokenKind.CompanyInit
import models.website.Website
@@ -32,6 +33,7 @@ import orchestrators.ReportOrchestrator.ReportCompanyChangeThresholdInDays
import orchestrators.ReportOrchestrator.validateNotGouvWebsite
import play.api.Logger
import play.api.i18n.MessagesApi
+import play.api.libs.json.JsObject
import play.api.libs.json.Json
import repositories.accesstoken.AccessTokenRepositoryInterface
import repositories.blacklistedemails.BlacklistedEmailsRepositoryInterface
@@ -52,6 +54,7 @@ import services.emails.EmailDefinitionsConsumer.ConsumerReportReadByProNotificat
import services.emails.EmailDefinitionsPro.ProNewReportNotification
import services.emails.EmailDefinitionsPro.ProResponseAcknowledgment
import services.emails.MailServiceInterface
+import tasks.company.CompanySearchResult
import tasks.company.CompanySyncServiceInterface
import utils.Constants.ActionEvent._
import utils.Constants.ActionEvent
@@ -64,6 +67,7 @@ import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.Period
import java.time.ZoneOffset
+import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalAmount
import java.util.UUID
import java.util.concurrent.TimeUnit
@@ -291,8 +295,10 @@ class ReportOrchestrator(
throw SpammerEmailBlocked(emailAddress)
} else ()
- private[orchestrators] def validateCompany(reportDraft: ReportDraft): Future[Done.type] = {
+ private[orchestrators] def validateCompany(reportDraft: ReportDraft): Future[Done.type] =
+ validateCompany(reportDraft.companyActivityCode, reportDraft.companySiret)
+ private def validateCompany(maybeActivityCode: Option[String], maybeSiret: Option[SIRET]): Future[Done.type] = {
def validateSiretExistsOnEntrepriseApi(siret: SIRET) = companySyncService
.companyBySiret(siret)
.recoverWith { case error =>
@@ -307,14 +313,14 @@ class ReportOrchestrator(
}
for {
- _ <- reportDraft.companyActivityCode match {
+ _ <- maybeActivityCode match {
// 84 correspond à l'administration publique, sauf quelques cas particuliers :
// - 84.30B : Complémentaires retraites donc pas publique
case Some(activityCode) if activityCode.startsWith("84.") && activityCode != "84.30B" =>
Future.failed(AppError.CannotReportPublicAdministration)
case _ => Future.unit
}
- _ <- reportDraft.companySiret match {
+ _ <- maybeSiret match {
case Some(siret) =>
// Try to check if siret exist in signal conso database
companyRepository.findBySiret(siret).flatMap {
@@ -1105,6 +1111,141 @@ class ReportOrchestrator(
.sortWith(_.count > _.count)
.slice(0, 10)
+ private def ensureReportReattributable(report: Report) =
+ report.websiteURL.websiteURL.isEmpty &&
+ report.websiteURL.host.isEmpty &&
+ report.influencer.isEmpty &&
+ report.barcodeProductId.isEmpty &&
+ report.train.isEmpty &&
+ report.station.isEmpty &&
+ report.companyId.isDefined
+
+ private def isReattributable(report: Report) = for {
+ proEvents <- eventRepository.getEvents(
+ report.id,
+ EventFilter(eventType = Some(EventType.PRO), action = Some(ActionEvent.REPORT_PRO_RESPONSE))
+ )
+ filteredProEvents = proEvents
+ .filter(event => event.details.as[IncomingReportResponse].responseType == ReportResponseType.NOT_CONCERNED)
+ .filter(_.creationDate.isAfter(OffsetDateTime.now().minusDays(15)))
+ consoEvents <- eventRepository.getEvents(
+ report.id,
+ EventFilter(eventType = Some(EventType.CONSO), action = Some(ActionEvent.REATTRIBUTE))
+ )
+ } yield
+ if (ensureReportReattributable(report) && consoEvents.isEmpty) filteredProEvents.headOption.map(_.creationDate)
+ else None
+
+ // Signalement "réattribuable" si :
+ // - Le signalement existe et il ne concerne pas un site web, un train etc. (voir méthode ensureReportReattributable)
+ // - Le pro a répondu 'MalAttribue' (voir isReattributable)
+ // - Le conso ne l'a pas déjà réattribué
+ // - La réponse du pro n'est pas trop vieille (moins de 15 jours)
+ def isReattributable(reportId: UUID): Future[JsObject] = for {
+ maybeReport <- reportRepository.get(reportId)
+ report <- maybeReport.liftTo[Future](ReportNotFound(reportId))
+ maybeProResponseDate <- isReattributable(report)
+ proResponseDate <- maybeProResponseDate match {
+ case Some(date) => Future.successful(date)
+ case None => Future.failed(ReportNotReattributable(reportId))
+ }
+ } yield Json.obj(
+ "creationDate" -> report.creationDate,
+ "tags" -> report.tags,
+ "companyName" -> report.companyName,
+ "daysToAnswer" -> (15 - ChronoUnit.DAYS.between(proResponseDate, OffsetDateTime.now()))
+ )
+
+ private def toCompany(companySearchResult: CompanySearchResult) =
+ Company(
+ siret = companySearchResult.siret,
+ name = companySearchResult.name.getOrElse(""),
+ address = companySearchResult.address,
+ activityCode = companySearchResult.activityCode,
+ isHeadOffice = companySearchResult.isHeadOffice,
+ isOpen = companySearchResult.isOpen,
+ isPublic = companySearchResult.isPublic,
+ brand = companySearchResult.brand,
+ commercialName = companySearchResult.commercialName,
+ establishmentCommercialName = companySearchResult.establishmentCommercialName
+ )
+
+ // On vérifie si le signalement est réattribuable
+ // On vérifie en plus que la réattribution n'est pas à la meme entreprise
+ def reattribute(
+ reportId: UUID,
+ companyCreation: CompanySearchResult,
+ metadata: ReportMetadataDraft,
+ consumerIp: ConsumerIp
+ ): Future[Report] = for {
+ maybeReport <- reportRepository.get(reportId)
+ report <- maybeReport.liftTo[Future](ReportNotFound(reportId))
+ maybeProResponseDate <- isReattributable(report)
+ _ <- if (maybeProResponseDate.nonEmpty) Future.unit else Future.failed(ReportNotReattributable(reportId))
+ _ <- validateCompany(companyCreation.activityCode, Some(companyCreation.siret))
+ _ <-
+ if (report.companySiret.contains(companyCreation.siret)) Future.failed(CantReattributeToTheSameCompany)
+ else Future.unit
+ company <- companyRepository.getOrCreate(companyCreation.siret, toCompany(companyCreation))
+ reportFilesMap <- reportFileOrchestrator.prefetchReportsFiles(List(reportId))
+ files = reportFilesMap.getOrElse(reportId, List.empty).filter(_.origin == ReportFileOrigin.Consumer)
+ newFiles <- files.traverse(f => reportFileOrchestrator.duplicate(f.id, f.filename, f.reportId))
+
+ reportDraft = ReportDraft(
+ gender = report.gender,
+ category = report.category,
+ subcategories = report.subcategories,
+ details = report.details,
+ influencer = report.influencer,
+ companyName = Some(company.name),
+ companyCommercialName = company.commercialName,
+ companyEstablishmentCommercialName = company.establishmentCommercialName,
+ companyBrand = company.brand,
+ companyAddress = Some(company.address),
+ companySiret = Some(company.siret),
+ companyActivityCode = company.activityCode,
+ websiteURL = report.websiteURL.websiteURL,
+ phone = report.phone,
+ firstName = report.firstName,
+ lastName = report.lastName,
+ email = report.email,
+ consumerPhone = report.consumerPhone,
+ consumerReferenceNumber = report.consumerReferenceNumber,
+ contactAgreement = report.contactAgreement,
+ employeeConsumer = report.employeeConsumer,
+ forwardToReponseConso = Some(report.forwardToReponseConso),
+ fileIds = newFiles.map(_.id),
+ vendor = report.vendor,
+ tags = report.tags,
+ reponseconsoCode = Some(report.reponseconsoCode),
+ ccrfCode = Some(report.ccrfCode),
+ lang = report.lang,
+ barcodeProductId = report.barcodeProductId,
+ metadata = Some(metadata),
+ train = report.train,
+ station = report.station,
+ rappelConsoId = report.rappelConsoId,
+ companyIsHeadOffice = None,
+ companyIsOpen = None,
+ companyIsPublic = None
+ )
+ createdReport <- createReport(reportDraft, consumerIp)
+ _ <- eventRepository.create(
+ Event(
+ UUID.randomUUID(),
+ Some(report.id),
+ report.companyId,
+ None,
+ OffsetDateTime.now(),
+ Constants.EventType.CONSO,
+ Constants.ActionEvent.REATTRIBUTE,
+ Json.obj(
+ "newReportId" -> createdReport.id,
+ "newCompanyId" -> createdReport.companyId
+ )
+ )
+ )
+ } yield createdReport
}
object ReportOrchestrator {
diff --git a/app/utils/Constants.scala b/app/utils/Constants.scala
index 53d1a74f..136c4990 100644
--- a/app/utils/Constants.scala
+++ b/app/utils/Constants.scala
@@ -119,6 +119,8 @@ object Constants {
object USER_ACCESS_CREATED extends ActionEventValue("UserAccessCreated")
object USER_ACCESS_REMOVED extends ActionEventValue("UserAccessRemoved")
+ object REATTRIBUTE extends ActionEventValue("Réattribution du signalement")
+
val actionEvents = Seq(
A_CONTACTER,
HORS_PERIMETRE,
diff --git a/app/utils/FrontRoute.scala b/app/utils/FrontRoute.scala
index 09098628..3d1b6833 100644
--- a/app/utils/FrontRoute.scala
+++ b/app/utils/FrontRoute.scala
@@ -17,6 +17,7 @@ class FrontRoute(signalConsoConfiguration: SignalConsoConfiguration) {
if (report.tags.contains(Telecom)) "/litige/telecom"
else "/litige"
)
+ def reattribute(report: Report) = url.resolve(s"reattribuer/${report.id}")
def reportReview(id: String)(evaluation: ResponseEvaluation) = url.resolve(
s"/avis/$id?evaluation=${evaluation.entryName}"
diff --git a/app/views/mails/consumer/reportToConsumerAcknowledgmentPro.scala.html b/app/views/mails/consumer/reportToConsumerAcknowledgmentPro.scala.html
index 292e2498..df22b03c 100644
--- a/app/views/mails/consumer/reportToConsumerAcknowledgmentPro.scala.html
+++ b/app/views/mails/consumer/reportToConsumerAcknowledgmentPro.scala.html
@@ -57,9 +57,13 @@
@Messages("ConsumerReportAckProEmail.signalementNonConcerne")
@ExistingReportResponse.translateResponseDetails(reportResponse).getOrElse("")
-
- @Html(Messages("ConsumerReportAckProEmail.signalementNonAccepteHelp"))
@Messages("ConsumerReportAckProEmail.youCanReattribute")
+ +@Messages("ConsumerReportAckProEmail.maxDelayToReattribute")
+@Messages("ConsumerReportAckProEmail.ifYouAreSure")