Skip to content

Commit

Permalink
Merge pull request #1814 from betagouv/master
Browse files Browse the repository at this point in the history
MEP
  • Loading branch information
eletallbetagouv authored Dec 24, 2024
2 parents 1df48d1 + 9da2b42 commit 18dcf58
Show file tree
Hide file tree
Showing 35 changed files with 535 additions and 231 deletions.
8 changes: 8 additions & 0 deletions app/controllers/CompanyController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import models.company.CompanyAddressUpdate
import models.company.CompanyCreation
import models.company.CompanyRegisteredSearch
import models.company.CompanyWithNbReports
import orchestrators.AlbertOrchestrator
import orchestrators.CompaniesVisibilityOrchestrator
import orchestrators.CompanyOrchestrator
import play.api.Logger
Expand All @@ -24,6 +25,7 @@ class CompanyController(
companyOrchestrator: CompanyOrchestrator,
val companyVisibilityOrch: CompaniesVisibilityOrchestrator,
val companyRepository: CompanyRepositoryInterface,
albertOrchestrator: AlbertOrchestrator,
authenticator: Authenticator[User],
controllerComponents: ControllerComponents
)(implicit val ec: ExecutionContext)
Expand Down Expand Up @@ -198,6 +200,12 @@ class CompanyController(
)
}

def getProblemsSeenByAlbert(id: UUID) = SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async {
for {
maybeResult <- albertOrchestrator.genProblemsForCompany(id)
} yield Ok(Json.toJson(maybeResult))
}

}

object CompanyObjects {
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/HtmlFromTemplateGenerator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class HtmlFromTemplateGenerator(messagesApi: MessagesApi, frontRoute: FrontRoute
reportData.engagementReviewOption,
reportData.companyEvents,
reportData.files
)(frontRoute = frontRoute, None, messagesProvider)
)(frontRoute = frontRoute, Some(user.userRole), messagesProvider)
}

}
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/ReportController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ class ReportController(

def createReport: Action[JsValue] = IpRateLimitedAction2.async(parse.json) { implicit request =>
implicit val userRole: Option[UserRole] = None

for {
draftReport <- request.parseBody[ReportDraft]()
createdReport <- reportOrchestrator.validateAndCreateReport(draftReport).recover {
consumerIp = ConsumerIp(request.remoteAddress)
createdReport <- reportOrchestrator.validateAndCreateReport(draftReport, consumerIp).recover {
case err: SpammerEmailBlocked =>
logger.warn(err.details)
reportOrchestrator.createFakeReportForBlacklistedUser(draftReport)
Expand Down
11 changes: 9 additions & 2 deletions app/controllers/error/AppError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,7 @@ object AppError {
override val title: String = "The report is too old to update its company."
override val details: String =
s"Action non autorisée. Le signalement est trop ancien (plus de ${ReportCompanyChangeThresholdInDays} jours) pour pouvoir changer l'entreprise."
override val titleForLogs: String = "review_already_exists"

override val titleForLogs: String = "report_too_old"
}

final case class ReportNotFound(reportId: UUID) extends NotFoundError {
Expand Down Expand Up @@ -641,4 +640,12 @@ object AppError {
override val titleForLogs: String = "invalid_filters"
}

final case object ReportIsInFinalStatus extends NotFoundError {
override val scErrorCode: String = "SC-0070"
override val title: String = "Cannot update report company because report status is in final status."
override val details: String =
s"Action non autorisée. Le signalement a déjà été répondu par le professionnel, il n'est donc plus possible de changer l'entreprise."
override val titleForLogs: String = "report_in_final_status"
}

}
14 changes: 10 additions & 4 deletions app/loader/SignalConsoApplicationLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,11 @@ class SignalConsoComponents(
val websitesOrchestrator =
new WebsitesOrchestrator(websiteRepository, companyRepository, reportRepository, reportOrchestrator)

val albertOrchestrator = new AlbertOrchestrator(
reportRepository,
albertService
)

val reportClosureTask = new ReportClosureTask(
actorSystem,
reportRepository,
Expand Down Expand Up @@ -638,7 +643,8 @@ class SignalConsoComponents(
companyAccessRepository,
reportAdminActionOrchestrator,
websiteRepository,
eventRepository
eventRepository,
engagementRepository
)(
actorSystem
)
Expand Down Expand Up @@ -692,9 +698,8 @@ class SignalConsoComponents(
actorSystem,
taskConfiguration,
companyRepository,
reportRepository,
taskRepository,
albertService
albertOrchestrator,
taskRepository
)

// Controller
Expand Down Expand Up @@ -770,6 +775,7 @@ class SignalConsoComponents(
companyOrchestrator,
companiesVisibilityOrchestrator,
companyRepository,
albertOrchestrator,
cookieAuthenticator,
controllerComponents
)
Expand Down
23 changes: 23 additions & 0 deletions app/models/albert/AlbertProblemsResult.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package models.albert

import play.api.libs.json.Json
import play.api.libs.json.OFormat

case class AlbertProblemsResult(
nbReportsUsed: Int,
problemsFound: Seq[AlbertProblem]
)

object AlbertProblemsResult {
implicit val format: OFormat[AlbertProblemsResult] = Json.format[AlbertProblemsResult]

}

case class AlbertProblem(
probleme: String,
signalements: Int
)
object AlbertProblem {
implicit val format: OFormat[AlbertProblem] = Json.format[AlbertProblem]

}
21 changes: 21 additions & 0 deletions app/models/report/ConsumerIp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package models.report

import play.api.libs.json.Format
import play.api.libs.json.Json
import repositories.PostgresProfile.api._
import slick.ast.BaseTypedType
import slick.jdbc.H2Profile.MappedColumnType
import slick.jdbc.JdbcType

case class ConsumerIp(value: String) extends AnyVal

object ConsumerIp {
implicit val ConsumerIpFormat: Format[ConsumerIp] = Json.valueFormat[ConsumerIp]

implicit val ConsumerIpColumnType: JdbcType[ConsumerIp] with BaseTypedType[ConsumerIp] =
MappedColumnType.base[ConsumerIp, String](
_.value,
ConsumerIp(_)
)

}
4 changes: 3 additions & 1 deletion app/models/report/reportmetadata/ReportMetadata.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package models.report.reportmetadata

import models.company.Address
import models.company.Company
import models.report.ConsumerIp
import models.report.Report
import play.api.libs.json.Json
import play.api.libs.json.Writes
Expand All @@ -15,7 +16,8 @@ case class ReportMetadata(
reportId: UUID,
isMobileApp: Boolean,
os: Option[Os],
assignedUserId: Option[UUID]
assignedUserId: Option[UUID],
consumerIp: Option[ConsumerIp]
)

object ReportMetadata {
Expand Down
7 changes: 5 additions & 2 deletions app/models/report/reportmetadata/ReportMetadataDraft.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package models.report.reportmetadata

import ai.x.play.json.Encoders.encoder
import ai.x.play.json.Jsonx
import cats.implicits.catsSyntaxOptionId
import models.report.ConsumerIp

import java.util.UUID
import scala.annotation.nowarn
Expand All @@ -11,12 +13,13 @@ case class ReportMetadataDraft(
isMobileApp: Boolean,
os: Option[Os]
) {
def toReportMetadata(reportId: UUID) =
def toReportMetadata(reportId: UUID, consumerIp: ConsumerIp) =
ReportMetadata(
reportId = reportId,
isMobileApp = isMobileApp,
os = os,
assignedUserId = None
assignedUserId = None,
consumerIp.some
)
}

Expand Down
17 changes: 12 additions & 5 deletions app/models/report/sampledata/SampleDataService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import models.UserRole.Professionnel
import models.company.AccessLevel
import models.company.Company
import models.report.ExistingResponseDetails.REMBOURSEMENT_OU_AVOIR
import models.report.ConsumerIp
import models.report.IncomingReportResponse
import models.report.ReportResponseType.ACCEPTED
import models.report.ReportResponseType.NOT_CONCERNED
Expand All @@ -19,6 +20,7 @@ import play.api.Logging
import repositories.accesstoken.AccessTokenRepositoryInterface
import repositories.company.CompanyRepositoryInterface
import repositories.companyaccess.CompanyAccessRepositoryInterface
import repositories.engagement.EngagementRepositoryInterface
import repositories.event.EventRepositoryInterface
import repositories.report.ReportRepositoryInterface
import repositories.user.UserRepositoryInterface
Expand All @@ -39,7 +41,8 @@ class SampleDataService(
companyAccessRepository: CompanyAccessRepositoryInterface,
reportAdminActionOrchestrator: ReportAdminActionOrchestrator,
websiteRepository: WebsiteRepositoryInterface,
eventRepository: EventRepositoryInterface
eventRepository: EventRepositoryInterface,
engagementRepository: EngagementRepositoryInterface
)(implicit system: ActorSystem)
extends Logging {

Expand Down Expand Up @@ -159,7 +162,7 @@ class SampleDataService(
s"--- Company access given to user"
)
reports = ReportGenerator.visibleReports(c)
createdReports <- reports.traverse(reportOrchestrator.createReport)
createdReports <- reports.traverse(reportOrchestrator.createReport(_, ConsumerIp("1.1.1.1")))
_ = logger.info(
s"--- Pending reports created"
)
Expand Down Expand Up @@ -219,7 +222,9 @@ class SampleDataService(
)

private def processedReports(c: Company, response: IncomingReportResponse, proUser: User) = for {
createdReports <- ReportGenerator.visibleReports(c).traverse(reportOrchestrator.createReport)
createdReports <- ReportGenerator
.visibleReports(c)
.traverse(reportOrchestrator.createReport(_, ConsumerIp("1.1.1.1")))
_ = logger.info(
s"--- Closed reports created"
)
Expand Down Expand Up @@ -269,8 +274,10 @@ class SampleDataService(
companyIds = companies.map(c => c.company.id)
reportList <- companyIds.flatTraverse(c => reportRepository.getReports(c))
_ = logger.info(s"Looking for reports link to company user ${predefinedUser.id}, found: ${reportList.size}")
_ <- reportList.traverse(r => reportAdminActionOrchestrator.deleteReport(r.id))
_ <- maybeUser.traverse(user => eventRepository.deleteByUserId(user.id))
_ <- reportList.traverse(r => reportAdminActionOrchestrator.deleteReport(r.id))
_ <- maybeUser.traverse { user =>
engagementRepository.removeByUserId(user.id).flatMap(_ => eventRepository.deleteByUserId(user.id))
}
websites <- websiteRepository.searchByCompaniesId(companies.map(_.company.id))
_ = logger.info(s"Looking for websites link to company user ${predefinedUser.id}, found: ${reportList.size}")
_ <- websites.map(_.id).traverse(websiteRepository.delete)
Expand Down
87 changes: 87 additions & 0 deletions app/orchestrators/AlbertOrchestrator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package orchestrators

import models.albert.AlbertProblemsResult
import models.company.Company
import models.report.Report
import play.api.Logger
import repositories.report.ReportRepositoryInterface
import services.AlbertService

import java.util.UUID
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

class AlbertOrchestrator(
val reportRepository: ReportRepositoryInterface,
val albertService: AlbertService
)(implicit ec: ExecutionContext) {
final val logger = Logger(getClass)

def genActivityLabelForCompany(company: Company): Future[Option[String]] = {
val maxReportsUsed = 5
for {
descriptions <- getLatestMeaningfulReportsDescsForCompany(
company.id,
maxReportsUsed
)
maybeLabel <- descriptions match {
case Nil =>
logger.info(s"Couldn't find enough usable reports for Albert for ${company.siret}")
Future.successful(None)
case _ =>
albertService
.labelCompanyActivity(company.id, descriptions)
.recover { err =>
logger.error(s"Didn't get a result from Albert for ${company.siret}", err)
None
}
}
} yield maybeLabel
}

def genProblemsForCompany(companyId: UUID): Future[Option[AlbertProblemsResult]] = {
val maxReportsUsed = 30
for {
descriptions <- getLatestMeaningfulReportsDescsForCompany(
companyId,
maxReportsUsed
)
maybeResult <- descriptions match {
case Nil =>
logger.info(s"Not enough usable reports for Albert for company ${companyId}")
Future.successful(None)
case _ =>
albertService
.findProblems(companyId, descriptions)
.recover { err =>
logger.error(s"Didn't get a result from Albert for company $companyId", err)
None
}

}
} yield maybeResult
}

private def getLatestMeaningfulReportsDescsForCompany(companyId: UUID, maxReports: Int): Future[Seq[String]] =
for {
allLatestReports <- reportRepository.getLatestReportsOfCompany(
companyId,
// we need a little big of margin:
// - not all reports will have a description
// - we deduplicate later by email
limit = maxReports * 2
)
meaningfulDescriptions =
// One user could submit several reports in a row on the company,
// thus distorting our data a bit
deduplicateByEmail(allLatestReports)
.flatMap(_.getDescription)
.take(maxReports)
} yield meaningfulDescriptions

private def deduplicateByEmail(reports: Seq[Report]) =
reports
.groupBy(_.email)
.map(_._2.head)
.toSeq
}
16 changes: 6 additions & 10 deletions app/orchestrators/CompanyOrchestrator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import tasks.company.CompanySearchResult.fromCompany
import utils.Constants.ActionEvent
import utils.Constants.EventType
import utils.FrontRoute
import utils.URL

import java.time.OffsetDateTime
import java.time.Period
Expand Down Expand Up @@ -160,15 +159,12 @@ class CompanyOrchestrator(
def searchSimilarCompanyByWebsite(url: String): Future[WebsiteCompanySearchResult] = {
logger.debug(s"searchCompaniesByHost $url")
for {
companiesByUrl <- websiteRepository.searchCompaniesByUrl(url)
(exact, similar) = companiesByUrl.partition(x => URL(url).getHost.contains(x._1.host))
similarHosts = similar.distinct
.take(3)
.map(w => WebsiteHost(w._1.host))
exactMatch = exact
.map { case (website, company) =>
CompanySearchResultApi.fromCompany(company, website)
}
companiesByUrl <- websiteRepository.searchCompaniesByUrl(url, 3)
(exact, similar) = companiesByUrl.partition { case ((_, distance), _) => distance == 0 }
similarHosts = similar.distinct.map(w => WebsiteHost(w._1._1.host))
exactMatch = exact.map { case (website, company) =>
CompanySearchResultApi.fromCompany(company, website._1)
}
_ = logger.debug(s"Found exactMatch: $exactMatch, similarHosts: ${similarHosts}")
} yield WebsiteCompanySearchResult(exactMatch, similarHosts)
}
Expand Down
Loading

0 comments on commit 18dcf58

Please sign in to comment.