From 9c8aa393ecd6b845a1d3af9fe3de7cf43e6077b1 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Sun, 13 Oct 2024 11:29:31 +0100 Subject: [PATCH 01/13] DRTII-1608 Start more flexible passengers end point --- server/src/main/resources/routes | 1 + .../application/SummariesController.scala | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/server/src/main/resources/routes b/server/src/main/resources/routes index 977129314..88fb5f85a 100644 --- a/server/src/main/resources/routes +++ b/server/src/main/resources/routes @@ -130,6 +130,7 @@ GET /api/arrivals/:startLocalDate/:endLocalDate/:terminalName + nocsrf GET /api/passengers/:startLocalDate/:endLocalDate controllers.application.SummariesController.exportPassengersByPortForDateRangeApi(startLocalDate: String, endLocalDate: String) GET /api/passengers/:startLocalDate/:endLocalDate/:terminalName controllers.application.SummariesController.exportPassengersByTerminalForDateRangeApi(startLocalDate: String, endLocalDate: String, terminalName: String) +GET /api/passengers controllers.application.SummariesController.exportPassengers() PUT /api/passengers/:localDate controllers.application.SummariesController.populatePassengersForDate(localDate: String) GET /debug/flights/:persistenceId/:dateString/:messages controllers.application.DebugController.getMessagesForFlightPersistenceIdAtTime(persistenceId: String, dateString: String, messages: Int) diff --git a/server/src/main/scala/controllers/application/SummariesController.scala b/server/src/main/scala/controllers/application/SummariesController.scala index 818393851..822f9bd4b 100644 --- a/server/src/main/scala/controllers/application/SummariesController.scala +++ b/server/src/main/scala/controllers/application/SummariesController.scala @@ -18,6 +18,7 @@ import uk.gov.homeoffice.drt.time.TimeZoneHelper.europeLondonTimeZone import uk.gov.homeoffice.drt.time.{DateRange, LocalDate, SDate} import scala.concurrent.Future +import scala.concurrent.duration.DurationInt import scala.util.{Failure, Success, Try} @@ -35,6 +36,30 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt Action(BadRequest(s"Invalid date format for $localDateStr. Expected YYYY-mm-dd")) } } + + def exportPassengers(): Action[AnyContent] = + auth( + Action { + request => + val maybeTerminal = request.getQueryString("terminal").map(t => Terminal(t)) + val granularity = request.getQueryString("granularity").getOrElse("total") + val duration = request.getQueryString("duration").map { d => + granularity match { + case "total" | "daily" => d.toInt.days + case "hourly" => d.toInt.hours + case "minute" => d.toInt.minutes + } + } + val start = request.getQueryString("start") match { + case None => SDate.now() + case Some("") => SDate.now() + } +// val terminal = Terminal(terminalName) +// val maybeTerminal = Option(terminal) +// exportPassengersCsv(startLocalDateString, endLocalDateString, request, maybeTerminal) + } + ) + def exportPassengersByTerminalForDateRangeApi(startLocalDateString: String, endLocalDateString: String, terminalName: String): Action[AnyContent] = From 3c43d82c60f25188ab81b8a7736261da9819ece7 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Tue, 15 Oct 2024 08:24:40 +0100 Subject: [PATCH 02/13] DRTII-1608 Add function to gather queues by granularity --- .../components/DashboardTerminalSummary.scala | 7 +- .../application/SummariesController.scala | 66 ++++++++++++++----- .../exports/StaffRequirementExports.scala | 17 ++--- .../src/main/scala/drt/shared/CrunchApi.scala | 21 +++--- 4 files changed, 73 insertions(+), 38 deletions(-) diff --git a/client/src/main/scala/drt/client/components/DashboardTerminalSummary.scala b/client/src/main/scala/drt/client/components/DashboardTerminalSummary.scala index b3e755806..d08775de5 100644 --- a/client/src/main/scala/drt/client/components/DashboardTerminalSummary.scala +++ b/client/src/main/scala/drt/client/components/DashboardTerminalSummary.scala @@ -118,7 +118,8 @@ object DashboardTerminalSummary { } } - def aggSplits(paxFeedSourceOrder: List[FeedSource], flights: Seq[ApiFlightWithSplits]): Map[PaxTypeAndQueue, Int] = BigSummaryBoxes.aggregateSplits(flights, paxFeedSourceOrder) + def aggSplits(paxFeedSourceOrder: List[FeedSource], flights: Seq[ApiFlightWithSplits]): Map[PaxTypeAndQueue, Int] = + BigSummaryBoxes.aggregateSplits(flights, paxFeedSourceOrder) case class Props(flights: List[ApiFlightWithSplits], crunchMinutes: List[CrunchMinute], @@ -133,8 +134,8 @@ object DashboardTerminalSummary { val component: Component[Props, Unit, Unit, CtorType.Props] = ScalaComponent.builder[Props]("SummaryBox") .render_P { props => - val crunchMinuteTimeSlots = groupCrunchMinutesByX(groupSize = 15)( - CrunchApi.terminalMinutesByMinute[CrunchMinute, CrunchMinute, TQM](props.crunchMinutes, props.terminal), + val crunchMinuteTimeSlots = groupCrunchMinutesBy(groupSize = 15)( + CrunchApi.terminalMinutesByMinute(props.crunchMinutes, props.terminal), props.terminal, Queues.queueOrder).flatMap(_._2) diff --git a/server/src/main/scala/controllers/application/SummariesController.scala b/server/src/main/scala/controllers/application/SummariesController.scala index 822f9bd4b..b2c285438 100644 --- a/server/src/main/scala/controllers/application/SummariesController.scala +++ b/server/src/main/scala/controllers/application/SummariesController.scala @@ -1,9 +1,13 @@ package controllers.application +import actors.PartitionedPortStateActor.GetMinutesForTerminalDateRange import akka.NotUsed +import akka.pattern.ask import akka.stream.scaladsl.Source import com.google.inject.Inject import controllers.application.exports.CsvFileStreaming.{makeFileName, sourceToCsvResponse, sourceToJsonResponse} +import drt.shared.CrunchApi.{CrunchMinute, MillisSinceEpoch, MinutesContainer} +import drt.shared.{CrunchApi, TQM} import play.api.mvc._ import spray.json.enrichAny import uk.gov.homeoffice.drt.auth.Roles.SuperAdmin @@ -15,10 +19,9 @@ import uk.gov.homeoffice.drt.ports.Queues.Queue import uk.gov.homeoffice.drt.ports.Terminals.Terminal import uk.gov.homeoffice.drt.ports.{PortRegion, Queues} import uk.gov.homeoffice.drt.time.TimeZoneHelper.europeLondonTimeZone -import uk.gov.homeoffice.drt.time.{DateRange, LocalDate, SDate} +import uk.gov.homeoffice.drt.time.{DateRange, LocalDate, SDate, SDateLike} import scala.concurrent.Future -import scala.concurrent.duration.DurationInt import scala.util.{Failure, Success, Try} @@ -41,25 +44,43 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt auth( Action { request => + val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) + val end = parseOptionalEndDate(request.getQueryString("end"), SDate.now()) + if (start > end) { + throw new Exception("Start date must be before end date") + } val maybeTerminal = request.getQueryString("terminal").map(t => Terminal(t)) val granularity = request.getQueryString("granularity").getOrElse("total") - val duration = request.getQueryString("duration").map { d => - granularity match { - case "total" | "daily" => d.toInt.days - case "hourly" => d.toInt.hours - case "minute" => d.toInt.minutes - } - } - val start = request.getQueryString("start") match { - case None => SDate.now() - case Some("") => SDate.now() + val acceptFormat = request.headers.get("Accept").getOrElse("application/json") + + queueTotalsForGranularity(start, end, maybeTerminal.getOrElse(Terminal("")).toString, granularity match { + case "hourly" => 60 + case "daily" => 1440 + case _ => 1 + }).map { queueTotals => + val fileName = makeFileName("passengers", maybeTerminal, start, end, airportConfig.portCode) + ".csv" + val contentStream = streamForGranularity(maybeTerminal, Some(granularity), acceptFormat) + val result = if (acceptFormat == "text/csv") + sourceToCsvResponse(contentStream(start.toLocalDate, end.toLocalDate), fileName) + else + sourceToJsonResponse(contentStream(start.toLocalDate, end.toLocalDate) + .fold(Seq[String]())(_ :+ _) + .map(objects => s"[${objects.mkString(",")}]")) } -// val terminal = Terminal(terminalName) -// val maybeTerminal = Option(terminal) -// exportPassengersCsv(startLocalDateString, endLocalDateString, request, maybeTerminal) + + Ok("done") + // val terminal = Terminal(terminalName) + // val maybeTerminal = Option(terminal) + // exportPassengersCsv(startLocalDateString, endLocalDateString, request, maybeTerminal) } ) + private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = + maybeString match { + case None => SDate.now() + case Some(dateStr) => SDate(dateStr) + } + def exportPassengersByTerminalForDateRangeApi(startLocalDateString: String, endLocalDateString: String, terminalName: String): Action[AnyContent] = @@ -106,6 +127,17 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt request.headers.get("Accept").getOrElse("application/json") } + val queueTotalsForGranularity: (SDateLike, SDateLike, Terminal, Int) => Future[Iterable[(Long, Seq[CrunchMinute])]] = + (start, end, terminal, granularity) => { + val request = GetMinutesForTerminalDateRange(start.millisSinceEpoch, end.millisSinceEpoch, terminal) + ctrl.actorService.queuesRouterActor.ask(request).mapTo[MinutesContainer[CrunchMinute, TQM]] + .map { minutesContainer => + val cms: List[CrunchMinute] = minutesContainer.minutes.map(_.toMinute).toList + val value: Seq[(MillisSinceEpoch, List[CrunchMinute])] = CrunchApi.terminalMinutesByMinute(cms, terminal) + CrunchApi.groupCrunchMinutesBy(granularity)(value, terminal, Queues.queueOrder) + } + } + private def streamForGranularity(maybeTerminal: Option[Terminal], granularity: Option[String], contentType: String, @@ -144,10 +176,10 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt } private val hourlyStream: (LocalDate => Future[Map[Long, Map[Queue, Int]]], LocalDate => Future[Map[Long, Int]]) => (LocalDate, LocalDate) => Source[(Map[Queue, Int], Int, Option[Long]), NotUsed] = - (queueTotalsForDate, capacityTotalsForDate) => (start, end) => + (queueTotalsForDate, hourlyCapacityTotalsForDate) => (start, end) => Source(DateRange(start, end)) .mapAsync(1) { date => - capacityTotalsForDate(date).map { capacityTotals => + hourlyCapacityTotalsForDate(date).map { capacityTotals => (date, capacityTotals) } } diff --git a/server/src/main/scala/services/exports/StaffRequirementExports.scala b/server/src/main/scala/services/exports/StaffRequirementExports.scala index 045cb85c0..870530068 100644 --- a/server/src/main/scala/services/exports/StaffRequirementExports.scala +++ b/server/src/main/scala/services/exports/StaffRequirementExports.scala @@ -80,14 +80,15 @@ object StaffRequirementExports { if (reqs.nonEmpty) reqs.max else 0 } - def groupByXMinutes[A, B](minutes: Seq[MinuteLike[A, B]], minutesInGroup: Int): Map[Int, Seq[A]] = minutes - .groupBy { sm => - val localSDate = SDate(sm.minute, europeLondonTimeZone) - ((localSDate.getHours * 60) + localSDate.getMinutes) / minutesInGroup - } - .view - .mapValues(_.map(_.toMinute)) - .toMap + def groupByXMinutes[A, B](minutes: Seq[MinuteLike[A, B]], minutesInGroup: Int): Map[Int, Seq[A]] = + minutes + .groupBy { sm => + val localSDate = SDate(sm.minute, europeLondonTimeZone) + ((localSDate.getHours * 60) + localSDate.getMinutes) / minutesInGroup + } + .view + .mapValues(_.map(_.toMinute)) + .toMap def staffingForLocalDateProvider(utcProvider: (UtcDate, UtcDate) => Source[(UtcDate, Seq[StaffMinute]), NotUsed]) (implicit ec: ExecutionContext, mat: Materializer): LocalDate => Future[Seq[StaffMinute]] = diff --git a/shared/src/main/scala/drt/shared/CrunchApi.scala b/shared/src/main/scala/drt/shared/CrunchApi.scala index 27b47239e..4601e6579 100644 --- a/shared/src/main/scala/drt/shared/CrunchApi.scala +++ b/shared/src/main/scala/drt/shared/CrunchApi.scala @@ -384,10 +384,11 @@ object CrunchApi { implicit val rw: ReadWriter[QueueHeadline] = macroRW } - def groupCrunchMinutesByX(groupSize: Int) - (crunchMinutes: Seq[(MillisSinceEpoch, List[CrunchMinute])], - terminalName: Terminal, - queueOrder: List[Queue]): Seq[(MillisSinceEpoch, Seq[CrunchMinute])] = { + def groupCrunchMinutesBy(groupSize: Int) + (crunchMinutes: Seq[(MillisSinceEpoch, List[CrunchMinute])], + terminalName: Terminal, + queueOrder: List[Queue], + ): Seq[(MillisSinceEpoch, Seq[CrunchMinute])] = crunchMinutes.grouped(groupSize).toList.map(group => { val byQueueName = group.flatMap(_._2).groupBy(_.queue) val startMinute = group.map(_._1).min @@ -420,13 +421,13 @@ object CrunchApi { } (startMinute, queueCrunchMinutes) }) - } def terminalMinutesByMinute[T <: MinuteLike[A, B], A, B](minutes: List[T], - terminalName: Terminal): Seq[(MillisSinceEpoch, List[T])] = minutes - .filter(_.terminal == terminalName) - .groupBy(_.minute) - .toList - .sortBy(_._1) + terminalName: Terminal): Seq[(MillisSinceEpoch, List[T])] = + minutes + .filter(_.terminal == terminalName) + .groupBy(_.minute) + .toList + .sortBy(_._1) } From 5251853aaaa7a655e5f9a764affa3d9d4b380e22 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Tue, 15 Oct 2024 15:56:37 +0100 Subject: [PATCH 03/13] DRTII-1608 Add json classes for export structure --- flights-export.json | 32 +++++++ queues-export.json | 57 +++++++++++++ .../application/SimulationsController.scala | 4 +- .../application/StaffingController.scala | 4 +- .../application/SummariesController.scala | 55 +++++------- .../exports/CsvFileStreaming.scala | 10 +-- .../exports/DesksExportController.scala | 8 +- .../exports/FlightsExportController.scala | 4 +- .../scala/services/exports/QueueExport.scala | 85 +++++++++++++++++++ .../scala/services/exports/ExportsSpec.scala | 8 +- .../services/exports/QueueExportSpec.scala | 65 ++++++++++++++ .../src/main/scala/drt/shared/CrunchApi.scala | 2 +- 12 files changed, 280 insertions(+), 54 deletions(-) create mode 100644 flights-export.json create mode 100644 queues-export.json create mode 100644 server/src/main/scala/services/exports/QueueExport.scala create mode 100644 server/src/test/scala/services/exports/QueueExportSpec.scala diff --git a/flights-export.json b/flights-export.json new file mode 100644 index 000000000..b80c5029c --- /dev/null +++ b/flights-export.json @@ -0,0 +1,32 @@ +{ + "start-time": "2024-10-15T12:07", + "end-time": "2024-10-15T14:06", + "ports": [ + { + "name": "LHR", + "terminals": [ + { + "name": "T2", + "flights": [ + { + "code": "BA0001", + "origin": "JFK", + "scheduled-time": "2024-10-15T12:05", + "estimated-pcp-arrival-start-time": "2024-10-15T12:15", + "estimated-pcp-arrival-end-time": "2024-10-15T12:19", + "estimated-passenger-count": 100 + }, + { + "code": "BA0002", + "origin": "CDG", + "scheduled-time": "2024-10-15T12:20", + "estimated-pcp-arrival-start-time": "2024-10-15T12:32", + "estimated-pcp-arrival-end-time": "2024-10-15T12:33", + "estimated-passenger-count": 50 + } + ] + } + ] + } + ] +} diff --git a/queues-export.json b/queues-export.json new file mode 100644 index 000000000..4c24bde3d --- /dev/null +++ b/queues-export.json @@ -0,0 +1,57 @@ +{ + "start-time": "2024-10-15T12:05", + "end-time": "2024-10-15T14:04", + "period-length-minutes": 15, + "ports": [ + { + "name": "LHR", + "terminals": [ + { + "name": "T1", + "periods": [ + { + "start-time": "2024-10-15T12:05", + "queues": [ + { + "name": "eea", + "pax-joining": 100, + "max-wait": 10 + }, + { + "name": "non-eea", + "pax-joining": 100, + "max-wait": 10 + }, + { + "name": "e-gates", + "pax-joining": 100, + "max-wait": 10 + } + ] + }, + { + "start-time": "2024-10-15T12:20", + "queues": [ + { + "name": "eea", + "pax-joining": 110, + "max-wait": 5 + }, + { + "name": "non-eea", + "pax-joining": 110, + "max-wait": 5 + }, + { + "name": "e-gates", + "pax-joining": 110, + "max-wait": 5 + } + ] + } + ] + } + ] + }, + ] +} diff --git a/server/src/main/scala/controllers/application/SimulationsController.scala b/server/src/main/scala/controllers/application/SimulationsController.scala index 7fc1d1832..da9e31841 100644 --- a/server/src/main/scala/controllers/application/SimulationsController.scala +++ b/server/src/main/scala/controllers/application/SimulationsController.scala @@ -151,8 +151,8 @@ class SimulationsController @Inject()(cc: ControllerComponents, ctrl: DrtSystemI val fileName = CsvFileStreaming.makeFileName(s"simulation-${simulationParams.passengerWeighting}", Option(simulationParams.terminal), - simulationParams.date, - simulationParams.date, + SDate(simulationParams.date), + SDate(simulationParams.date), airportConfig.portCode ) + ".csv" diff --git a/server/src/main/scala/controllers/application/StaffingController.scala b/server/src/main/scala/controllers/application/StaffingController.scala index 44381227e..ad85c0cad 100644 --- a/server/src/main/scala/controllers/application/StaffingController.scala +++ b/server/src/main/scala/controllers/application/StaffingController.scala @@ -147,8 +147,8 @@ class StaffingController @Inject()(cc: ControllerComponents, CsvFileStreaming.makeFileName( "staff-movements", Option(terminal), - localDate, - localDate, + SDate(localDate), + SDate(localDate), airportConfig.portCode ) + ".csv" ) diff --git a/server/src/main/scala/controllers/application/SummariesController.scala b/server/src/main/scala/controllers/application/SummariesController.scala index b2c285438..22611cecb 100644 --- a/server/src/main/scala/controllers/application/SummariesController.scala +++ b/server/src/main/scala/controllers/application/SummariesController.scala @@ -3,13 +3,16 @@ package controllers.application import actors.PartitionedPortStateActor.GetMinutesForTerminalDateRange import akka.NotUsed import akka.pattern.ask -import akka.stream.scaladsl.Source +import akka.stream.Materializer +import akka.stream.scaladsl.{Sink, Source} import com.google.inject.Inject import controllers.application.exports.CsvFileStreaming.{makeFileName, sourceToCsvResponse, sourceToJsonResponse} +import controllers.model.RedListCountsJsonFormats.SDateJsonFormat import drt.shared.CrunchApi.{CrunchMinute, MillisSinceEpoch, MinutesContainer} import drt.shared.{CrunchApi, TQM} import play.api.mvc._ -import spray.json.enrichAny +import services.exports.QueueExport +import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat, enrichAny} import uk.gov.homeoffice.drt.auth.Roles.SuperAdmin import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.db.dao.{CapacityHourlyDao, PassengersHourlyDao} @@ -25,7 +28,7 @@ import scala.concurrent.Future import scala.util.{Failure, Success, Try} -class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) { +class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) { def populatePassengersForDate(localDateStr: String): Action[AnyContent] = authByRole(SuperAdmin) { LocalDate.parse(localDateStr) match { case Some(localDate) => @@ -40,6 +43,8 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt } } + val queueExport = QueueExport(queueTotalsForGranularity, ctrl.airportConfig.terminals, ctrl.airportConfig.portCode) + def exportPassengers(): Action[AnyContent] = auth( Action { @@ -49,35 +54,17 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt if (start > end) { throw new Exception("Start date must be before end date") } - val maybeTerminal = request.getQueryString("terminal").map(t => Terminal(t)) - val granularity = request.getQueryString("granularity").getOrElse("total") - val acceptFormat = request.headers.get("Accept").getOrElse("application/json") - - queueTotalsForGranularity(start, end, maybeTerminal.getOrElse(Terminal("")).toString, granularity match { - case "hourly" => 60 - case "daily" => 1440 - case _ => 1 - }).map { queueTotals => - val fileName = makeFileName("passengers", maybeTerminal, start, end, airportConfig.portCode) + ".csv" - val contentStream = streamForGranularity(maybeTerminal, Some(granularity), acceptFormat) - val result = if (acceptFormat == "text/csv") - sourceToCsvResponse(contentStream(start.toLocalDate, end.toLocalDate), fileName) - else - sourceToJsonResponse(contentStream(start.toLocalDate, end.toLocalDate) - .fold(Seq[String]())(_ :+ _) - .map(objects => s"[${objects.mkString(",")}]")) - } + val periodMinutes = request.getQueryString("period-minutes").map(_.toInt).getOrElse(15) + + val portJson = queueExport(start, end, periodMinutes) Ok("done") - // val terminal = Terminal(terminalName) - // val maybeTerminal = Option(terminal) - // exportPassengersCsv(startLocalDateString, endLocalDateString, request, maybeTerminal) } ) private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = maybeString match { - case None => SDate.now() + case None => default case Some(dateStr) => SDate(dateStr) } @@ -103,7 +90,7 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt ): Result = (LocalDate.parse(startLocalDateString), LocalDate.parse(endLocalDateString)) match { case (Some(start), Some(end)) => - val fileName = makeFileName("passengers", maybeTerminal, start, end, airportConfig.portCode) + ".csv" + val fileName = makeFileName("passengers", maybeTerminal, SDate(start), SDate(end), airportConfig.portCode) + ".csv" val contentStream = streamForGranularity(maybeTerminal, request.getQueryString("granularity"), acceptHeader(request)) val result = if (acceptHeader(request) == "text/csv") @@ -129,14 +116,14 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt val queueTotalsForGranularity: (SDateLike, SDateLike, Terminal, Int) => Future[Iterable[(Long, Seq[CrunchMinute])]] = (start, end, terminal, granularity) => { - val request = GetMinutesForTerminalDateRange(start.millisSinceEpoch, end.millisSinceEpoch, terminal) - ctrl.actorService.queuesRouterActor.ask(request).mapTo[MinutesContainer[CrunchMinute, TQM]] - .map { minutesContainer => - val cms: List[CrunchMinute] = minutesContainer.minutes.map(_.toMinute).toList - val value: Seq[(MillisSinceEpoch, List[CrunchMinute])] = CrunchApi.terminalMinutesByMinute(cms, terminal) - CrunchApi.groupCrunchMinutesBy(granularity)(value, terminal, Queues.queueOrder) - } - } + val request = GetMinutesForTerminalDateRange(start.millisSinceEpoch, end.millisSinceEpoch, terminal) + ctrl.actorService.queuesRouterActor.ask(request).mapTo[MinutesContainer[CrunchMinute, TQM]] + .map { minutesContainer => + val cms: List[CrunchMinute] = minutesContainer.minutes.map(_.toMinute).toList + val value: Seq[(MillisSinceEpoch, List[CrunchMinute])] = CrunchApi.terminalMinutesByMinute(cms, terminal) + CrunchApi.groupCrunchMinutesBy(granularity)(value, terminal, Queues.queueOrder) + } + } private def streamForGranularity(maybeTerminal: Option[Terminal], granularity: Option[String], diff --git a/server/src/main/scala/controllers/application/exports/CsvFileStreaming.scala b/server/src/main/scala/controllers/application/exports/CsvFileStreaming.scala index 6358af1c1..16a816e3e 100644 --- a/server/src/main/scala/controllers/application/exports/CsvFileStreaming.scala +++ b/server/src/main/scala/controllers/application/exports/CsvFileStreaming.scala @@ -8,7 +8,7 @@ import play.api.mvc.{ResponseHeader, Result} import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.ports.Terminals.Terminal import uk.gov.homeoffice.drt.time.TimeZoneHelper.europeLondonTimeZone -import uk.gov.homeoffice.drt.time.{LocalDate, SDate} +import uk.gov.homeoffice.drt.time.{SDate, SDateLike} object CsvFileStreaming { @@ -41,11 +41,11 @@ object CsvFileStreaming { def makeFileName(subject: String, maybeTerminal: Option[Terminal], - start: LocalDate, - end: LocalDate, + start: SDateLike, + end: SDateLike, portCode: PortCode): String = { - val startLocal = SDate(SDate(start), europeLondonTimeZone) - val endLocal = SDate(SDate(end), europeLondonTimeZone) + val startLocal = SDate(start, europeLondonTimeZone) + val endLocal = SDate(end, europeLondonTimeZone) val endDate = if (startLocal.daysBetweenInclusive(endLocal) > 1) f"-to-${endLocal.getFullYear}-${endLocal.getMonth}%02d-${endLocal.getDate}%02d" else "" diff --git a/server/src/main/scala/controllers/application/exports/DesksExportController.scala b/server/src/main/scala/controllers/application/exports/DesksExportController.scala index d3558cb1e..9db4b07fb 100644 --- a/server/src/main/scala/controllers/application/exports/DesksExportController.scala +++ b/server/src/main/scala/controllers/application/exports/DesksExportController.scala @@ -34,7 +34,7 @@ class DesksExportController @Inject()(cc: ControllerComponents, ctrl: DrtSystemI exportBetweenTimestampsCSV( deskRecsExportStreamForTerminalDates(pointInTime = Option(pit.millisSinceEpoch), start, end, Terminal(terminalName), periodMinutes(request)), - makeFileName(s"desks-and-queues-recs-at-${pit.toISOString}-for", Option(Terminal(terminalName)), start.toLocalDate, end.toLocalDate, airportConfig.portCode) + ".csv", + makeFileName(s"desks-and-queues-recs-at-${pit.toISOString}-for", Option(Terminal(terminalName)), start, end, airportConfig.portCode) + ".csv", ) case _ => Action(BadRequest(write(ErrorResponse("Invalid date format")))) @@ -58,7 +58,7 @@ class DesksExportController @Inject()(cc: ControllerComponents, ctrl: DrtSystemI exportBetweenTimestampsCSV( deskRecsExportStreamForTerminalDates(pointInTime = None, start, end, Terminal(terminalName), periodMinutes(request)), - makeFileName("desks-and-queues-recs", Option(Terminal(terminalName)), start.toLocalDate, end.toLocalDate, airportConfig.portCode) + ".csv", + makeFileName("desks-and-queues-recs", Option(Terminal(terminalName)), start, end, airportConfig.portCode) + ".csv", ) case _ => Action(BadRequest(write(ErrorResponse("Invalid date format")))) @@ -79,7 +79,7 @@ class DesksExportController @Inject()(cc: ControllerComponents, ctrl: DrtSystemI exportBetweenTimestampsCSV( deploymentsExportStreamForTerminalDates(pointInTime = Option(pit.millisSinceEpoch), start, end, Terminal(terminalName), periodMinutes(request)), - makeFileName(s"desks-and-queues-deps-at-${pit.toISOString}-for", Option(Terminal(terminalName)), start.toLocalDate, end.toLocalDate, airportConfig.portCode) + ".csv", + makeFileName(s"desks-and-queues-deps-at-${pit.toISOString}-for", Option(Terminal(terminalName)), start, end, airportConfig.portCode) + ".csv", ) case _ => Action(BadRequest(write(ErrorResponse("Invalid date format")))) @@ -99,7 +99,7 @@ class DesksExportController @Inject()(cc: ControllerComponents, ctrl: DrtSystemI exportBetweenTimestampsCSV( deploymentsExportStreamForTerminalDates(pointInTime = None, start, end, Terminal(terminalName), periodMinutes(request)), - makeFileName("desks-and-queues-deps", Option(Terminal(terminalName)), start.toLocalDate, end.toLocalDate, airportConfig.portCode) + ".csv", + makeFileName("desks-and-queues-deps", Option(Terminal(terminalName)), start, end, airportConfig.portCode) + ".csv", ) case _ => Action(BadRequest(write(ErrorResponse("Invalid date format")))) diff --git a/server/src/main/scala/controllers/application/exports/FlightsExportController.scala b/server/src/main/scala/controllers/application/exports/FlightsExportController.scala index f99ee8f6c..c6d8444e5 100644 --- a/server/src/main/scala/controllers/application/exports/FlightsExportController.scala +++ b/server/src/main/scala/controllers/application/exports/FlightsExportController.scala @@ -84,7 +84,7 @@ class FlightsExportController @Inject()(cc: ControllerComponents, ctrl: DrtSyste val getManifests = FlightExports.manifestsForLocalDateProvider(ctrl.applicationService.manifestsProvider) val toRows = FlightExports.dateAndFlightsToCsvRows(ctrl.airportConfig.portCode, terminal, ctrl.feedService.paxFeedSourceOrder, getManifests) val csvStream = GeneralExport.toCsv(start, end, getFlights, toRows) - val fileName = makeFileName("flights", Option(terminal), start, end, airportConfig.portCode) + ".csv" + val fileName = makeFileName("flights", Option(terminal), SDate(start), SDate(end), airportConfig.portCode) + ".csv" Try(sourceToCsvResponse(csvStream, fileName)) match { case Success(value) => value case Failure(t) => @@ -128,7 +128,7 @@ class FlightsExportController @Inject()(cc: ControllerComponents, ctrl: DrtSyste ctrl.applicationService.manifestsProvider(d, d).map(_._2).runFold(VoyageManifests.empty)(_ ++ _).map(m => (fws, m)) } val csvStream = export.csvStream(flightsAndManifestsStream) - val fileName = makeFileName("flights", Option(export.terminal), export.start.toLocalDate, export.end.toLocalDate, airportConfig.portCode) + ".csv" + val fileName = makeFileName("flights", Option(export.terminal), export.start, export.end, airportConfig.portCode) + ".csv" Try(sourceToCsvResponse(csvStream, fileName)) match { case Success(value) => value case Failure(t) => diff --git a/server/src/main/scala/services/exports/QueueExport.scala b/server/src/main/scala/services/exports/QueueExport.scala new file mode 100644 index 000000000..9255cf6bf --- /dev/null +++ b/server/src/main/scala/services/exports/QueueExport.scala @@ -0,0 +1,85 @@ +package services.exports + +import akka.stream.Materializer +import akka.stream.scaladsl.{Sink, Source} +import controllers.model.RedListCountsJsonFormats.SDateJsonFormat +import drt.shared.CrunchApi.{CrunchMinute, MillisSinceEpoch} +import services.exports.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat} +import uk.gov.homeoffice.drt.ports.PortCode +import uk.gov.homeoffice.drt.ports.Queues.Queue +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.time.{SDate, SDateLike} + +import scala.concurrent.{ExecutionContext, Future} + +trait QueueExportJsonProtocol extends DefaultJsonProtocol { + implicit object QueueJsonFormat extends RootJsonFormat[Queue] { + override def write(obj: Queue): JsValue = JsString(obj.stringValue) + + override def read(json: JsValue): Queue = json match { + case JsString(value) => Queue(value) + case unexpected => throw new Exception(s"Failed to parse Queue. Expected JsString. Got ${unexpected.getClass}") + } + } + + implicit val queueJsonFormat: RootJsonFormat[QueueJson] = jsonFormat3(QueueJson.apply) + + implicit val periodJsonFormat: RootJsonFormat[PeriodJson] = jsonFormat2(PeriodJson.apply) + + implicit object TerminalJsonFormat extends RootJsonFormat[Terminal] { + override def write(obj: Terminal): JsValue = JsString(obj.toString) + + override def read(json: JsValue): Terminal = json match { + case JsString(value) => Terminal(value) + case unexpected => throw new Exception(s"Failed to parse Terminal. Expected JsString. Got ${unexpected.getClass}") + } + } + + implicit val terminalQueuesJsonFormat: RootJsonFormat[TerminalQueuesJson] = jsonFormat2(TerminalQueuesJson.apply) + + implicit val portCodeJsonFormat: RootJsonFormat[PortCode] = jsonFormat1(PortCode.apply) + + implicit val portQueuesJsonFormat: RootJsonFormat[PortQueuesJson] = jsonFormat2(PortQueuesJson.apply) + +} + +object QueueExport { + + case class QueueJson(name: Queue, incomingPax: Int, maxWaitMinutes: Int) + + object QueueJson { + def apply(cm: CrunchMinute): QueueJson = QueueJson(cm.queue, cm.paxLoad.toInt, cm.waitTime) + } + + case class PeriodJson(startTime: SDateLike, queues: Iterable[QueueJson]) + + case class TerminalQueuesJson(terminal: Terminal, periods: Iterable[PeriodJson]) + + case class PortQueuesJson(portCode: PortCode, terminals: Iterable[TerminalQueuesJson]) + + def apply(minutesSource: (SDateLike, SDateLike, Terminal, Int) => Future[Iterable[(Long, Seq[CrunchMinute])]], + terminals: Iterable[Terminal], + portCode: PortCode, + ) + (implicit mat: Materializer, ec: ExecutionContext): (SDateLike, SDateLike, Int) => Future[PortQueuesJson] = + (start, end, periodMinutes) => { + Source(terminals.toSeq) + .mapAsync(terminals.size) { terminal => + minutesSource(start, end, terminal, periodMinutes) + .map { queueTotals: Iterable[(MillisSinceEpoch, Seq[CrunchMinute])] => + queueTotals.map { case (slotTime, queues) => + PeriodJson(SDate(slotTime), queues.map(QueueJson.apply)) + } + } + } + .runWith(Sink.seq) + .map { + terminalPeriods => + val terminalQueues = terminals.zip(terminalPeriods).map { + case (terminal, periods) => TerminalQueuesJson(terminal, periods) + } + PortQueuesJson(portCode, terminalQueues) + } + } +} diff --git a/server/src/test/scala/services/exports/ExportsSpec.scala b/server/src/test/scala/services/exports/ExportsSpec.scala index bf3285e0d..8c0ac3528 100644 --- a/server/src/test/scala/services/exports/ExportsSpec.scala +++ b/server/src/test/scala/services/exports/ExportsSpec.scala @@ -13,7 +13,7 @@ class ExportsSpec extends Specification { "I should get a file name with just the start date 2020-06-24" >> { val startDate = SDate("2020-06-24T00:00", europeLondonTimeZone) val endDate = startDate.addDays(1).addMinutes(-1) - val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate.toLocalDate, endDate.toLocalDate, PortCode("LHR")) + val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate, endDate, PortCode("LHR")) val expected = "LHR-T1-mysubject-2020-06-24" @@ -27,7 +27,7 @@ class ExportsSpec extends Specification { "I should get a file name with just the start date" >> { val startDate = SDate("2020-01-01T00:00", europeLondonTimeZone) val endDate = startDate.addDays(1).addMinutes(-1) - val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate.toLocalDate, endDate.toLocalDate, PortCode("LHR")) + val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate, endDate, PortCode("LHR")) val expected = "LHR-T1-mysubject-2020-01-01" @@ -41,7 +41,7 @@ class ExportsSpec extends Specification { "I should get a file name with the start date 2020-06-24 and end date of 2020-06-25" >> { val startDate = SDate("2020-06-24T00:00", europeLondonTimeZone) val endDate = startDate.addDays(2).addMinutes(-1) - val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate.toLocalDate, endDate.toLocalDate, PortCode("LHR")) + val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate, endDate, PortCode("LHR")) val expected = "LHR-T1-mysubject-2020-06-24-to-2020-06-25" @@ -55,7 +55,7 @@ class ExportsSpec extends Specification { "I should get a file name with the start date 2020-01-01 and end date of 2020-01-02" >> { val startDate = SDate("2020-01-01T00:00", europeLondonTimeZone) val endDate = startDate.addDays(2).addMinutes(-1) - val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate.toLocalDate, endDate.toLocalDate, PortCode("LHR")) + val result = CsvFileStreaming.makeFileName("mysubject", Option(T1), startDate, endDate, PortCode("LHR")) val expected = "LHR-T1-mysubject-2020-01-01-to-2020-01-02" diff --git a/server/src/test/scala/services/exports/QueueExportSpec.scala b/server/src/test/scala/services/exports/QueueExportSpec.scala new file mode 100644 index 000000000..a89de9397 --- /dev/null +++ b/server/src/test/scala/services/exports/QueueExportSpec.scala @@ -0,0 +1,65 @@ +package services.exports + +import akka.actor.ActorSystem +import akka.stream.Materializer +import drt.shared.CrunchApi.CrunchMinute +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import services.exports.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import uk.gov.homeoffice.drt.ports.PortCode +import uk.gov.homeoffice.drt.ports.Queues.{EGate, EeaDesk, NonEeaDesk} +import uk.gov.homeoffice.drt.ports.Terminals.{T1, Terminal} +import uk.gov.homeoffice.drt.time.{SDate, SDateLike} + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, ExecutionContextExecutor, Future} + + +class QueueExportSpec extends AnyWordSpec with Matchers { + implicit val system: ActorSystem = ActorSystem("QueueExportSpec") + implicit val mat: Materializer = Materializer.matFromSystem + implicit val ec: ExecutionContextExecutor = system.dispatcher + + val minute: SDateLike = SDate("2024-10-15T12:00") + + "QueueExport" should { + "return a PortQueuesJson with the correct structure" in { + val source = (_: SDateLike, _: SDateLike, _: Terminal, _: Int) => { + Future.successful(Seq( + minute.millisSinceEpoch -> Seq( + CrunchMinute(T1, EeaDesk, minute.millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, NonEeaDesk, minute.millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, EGate, minute.millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), + ), + minute.addMinutes(15).millisSinceEpoch -> Seq( + CrunchMinute(T1, EeaDesk, minute.addMinutes(15).millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, NonEeaDesk, minute.addMinutes(15).millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, EGate, minute.addMinutes(15).millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), + ), + )) + } + val export = QueueExport(source, Seq(T1), PortCode("LHR")) + Await.result(export(minute, minute, 15), 1.second) shouldEqual + PortQueuesJson( + PortCode("LHR"), + Seq( + TerminalQueuesJson( + T1, + Seq( + PeriodJson(minute, Seq( + QueueJson(EeaDesk, 10, 0), + QueueJson(NonEeaDesk, 12, 0), + QueueJson(EGate, 14, 0), + )), + PeriodJson(minute.addMinutes(15), Seq( + QueueJson(EeaDesk, 10, 0), + QueueJson(NonEeaDesk, 12, 0), + QueueJson(EGate, 14, 0), + )), + ) + ) + ) + ) + } + } +} diff --git a/shared/src/main/scala/drt/shared/CrunchApi.scala b/shared/src/main/scala/drt/shared/CrunchApi.scala index 4601e6579..117c779c8 100644 --- a/shared/src/main/scala/drt/shared/CrunchApi.scala +++ b/shared/src/main/scala/drt/shared/CrunchApi.scala @@ -422,7 +422,7 @@ object CrunchApi { (startMinute, queueCrunchMinutes) }) - def terminalMinutesByMinute[T <: MinuteLike[A, B], A, B](minutes: List[T], + def terminalMinutesByMinute[T <: MinuteLike[_, _]](minutes: List[T], terminalName: Terminal): Seq[(MillisSinceEpoch, List[T])] = minutes .filter(_.terminal == terminalName) From 2e0771fe412c4d7a7c82f67d7250dd78798bd168 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Wed, 16 Oct 2024 11:23:22 +0100 Subject: [PATCH 04/13] DRTII-1608 Version api. Fix forward ref --- server/src/main/resources/routes | 2 +- .../application/SummariesController.scala | 41 +------------ .../api/v1/QueuesApiController.scala | 59 +++++++++++++++++++ .../{exports => api}/QueueExport.scala | 34 +---------- .../serialisation/QueueApiJsonProtocol.scala | 48 +++++++++++++++ .../services/exports/QueueExportSpec.scala | 3 +- 6 files changed, 113 insertions(+), 74 deletions(-) create mode 100644 server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala rename server/src/main/scala/services/{exports => api}/QueueExport.scala (54%) create mode 100644 server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala diff --git a/server/src/main/resources/routes b/server/src/main/resources/routes index 88fb5f85a..e9a33b389 100644 --- a/server/src/main/resources/routes +++ b/server/src/main/resources/routes @@ -130,7 +130,7 @@ GET /api/arrivals/:startLocalDate/:endLocalDate/:terminalName + nocsrf GET /api/passengers/:startLocalDate/:endLocalDate controllers.application.SummariesController.exportPassengersByPortForDateRangeApi(startLocalDate: String, endLocalDate: String) GET /api/passengers/:startLocalDate/:endLocalDate/:terminalName controllers.application.SummariesController.exportPassengersByTerminalForDateRangeApi(startLocalDate: String, endLocalDate: String, terminalName: String) -GET /api/passengers controllers.application.SummariesController.exportPassengers() +GET /api/v1/passengers controllers.application.api.v1.QueuesApiController.queues() PUT /api/passengers/:localDate controllers.application.SummariesController.populatePassengersForDate(localDate: String) GET /debug/flights/:persistenceId/:dateString/:messages controllers.application.DebugController.getMessagesForFlightPersistenceIdAtTime(persistenceId: String, dateString: String, messages: Int) diff --git a/server/src/main/scala/controllers/application/SummariesController.scala b/server/src/main/scala/controllers/application/SummariesController.scala index 22611cecb..208ca8dd4 100644 --- a/server/src/main/scala/controllers/application/SummariesController.scala +++ b/server/src/main/scala/controllers/application/SummariesController.scala @@ -1,18 +1,11 @@ package controllers.application -import actors.PartitionedPortStateActor.GetMinutesForTerminalDateRange import akka.NotUsed -import akka.pattern.ask -import akka.stream.Materializer -import akka.stream.scaladsl.{Sink, Source} +import akka.stream.scaladsl.Source import com.google.inject.Inject import controllers.application.exports.CsvFileStreaming.{makeFileName, sourceToCsvResponse, sourceToJsonResponse} -import controllers.model.RedListCountsJsonFormats.SDateJsonFormat -import drt.shared.CrunchApi.{CrunchMinute, MillisSinceEpoch, MinutesContainer} -import drt.shared.{CrunchApi, TQM} import play.api.mvc._ -import services.exports.QueueExport -import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat, enrichAny} +import spray.json.enrichAny import uk.gov.homeoffice.drt.auth.Roles.SuperAdmin import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.db.dao.{CapacityHourlyDao, PassengersHourlyDao} @@ -43,25 +36,6 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt } } - val queueExport = QueueExport(queueTotalsForGranularity, ctrl.airportConfig.terminals, ctrl.airportConfig.portCode) - - def exportPassengers(): Action[AnyContent] = - auth( - Action { - request => - val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) - val end = parseOptionalEndDate(request.getQueryString("end"), SDate.now()) - if (start > end) { - throw new Exception("Start date must be before end date") - } - val periodMinutes = request.getQueryString("period-minutes").map(_.toInt).getOrElse(15) - - val portJson = queueExport(start, end, periodMinutes) - - Ok("done") - } - ) - private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = maybeString match { case None => default @@ -114,17 +88,6 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt request.headers.get("Accept").getOrElse("application/json") } - val queueTotalsForGranularity: (SDateLike, SDateLike, Terminal, Int) => Future[Iterable[(Long, Seq[CrunchMinute])]] = - (start, end, terminal, granularity) => { - val request = GetMinutesForTerminalDateRange(start.millisSinceEpoch, end.millisSinceEpoch, terminal) - ctrl.actorService.queuesRouterActor.ask(request).mapTo[MinutesContainer[CrunchMinute, TQM]] - .map { minutesContainer => - val cms: List[CrunchMinute] = minutesContainer.minutes.map(_.toMinute).toList - val value: Seq[(MillisSinceEpoch, List[CrunchMinute])] = CrunchApi.terminalMinutesByMinute(cms, terminal) - CrunchApi.groupCrunchMinutesBy(granularity)(value, terminal, Queues.queueOrder) - } - } - private def streamForGranularity(maybeTerminal: Option[Terminal], granularity: Option[String], contentType: String, diff --git a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala new file mode 100644 index 000000000..e2897231c --- /dev/null +++ b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala @@ -0,0 +1,59 @@ +package controllers.application.api.v1 + +import actors.PartitionedPortStateActor.GetMinutesForTerminalDateRange +import akka.pattern.ask +import com.google.inject.Inject +import controllers.application.AuthController +import drt.shared.CrunchApi.{CrunchMinute, MinutesContainer} +import drt.shared.{CrunchApi, TQM} +import play.api.mvc._ +import services.api.QueueExport +import services.api.v1.serialisation.QueueApiJsonProtocol +import spray.json.enrichAny +import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface +import uk.gov.homeoffice.drt.ports.Queues +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.time.{SDate, SDateLike} + +import scala.concurrent.Future + + +class QueuesApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) with QueueApiJsonProtocol { + private val queueTotalsForGranularity: (SDateLike, SDateLike, Terminal, Int) => Future[Iterable[(Long, Seq[CrunchMinute])]] = + (start, end, terminal, granularity) => { + val request = GetMinutesForTerminalDateRange(start.millisSinceEpoch, end.millisSinceEpoch, terminal) + ctrl.actorService.queuesRouterActor.ask(request).mapTo[MinutesContainer[CrunchMinute, TQM]] + .map { minutesContainer => + val cms = minutesContainer.minutes.map(_.toMinute).toList + val cmsByMinute = CrunchApi.terminalMinutesByMinute(cms, terminal) + CrunchApi.groupCrunchMinutesBy(granularity)(cmsByMinute, terminal, Queues.queueOrder) + } + } + + private val queueExport: (SDateLike, SDateLike, Int) => Future[QueueExport.PortQueuesJson] = + QueueExport(queueTotalsForGranularity, ctrl.airportConfig.terminals, ctrl.airportConfig.portCode) + + def queues(): Action[AnyContent] = + auth( + Action.async { + request => + val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) + val end = parseOptionalEndDate(request.getQueryString("end"), SDate.now()) + if (start > end) { + throw new Exception("Start date must be before end date") + } + val periodMinutes = request.getQueryString("period-minutes").map(_.toInt).getOrElse(15) + + log.info(s"\n\nGetting queues for ${start.toISOString} -> ${end.toISOString} every $periodMinutes minutes\n\n") + + queueExport(start, end, periodMinutes) + .map(r => Ok(r.toJson.compactPrint)) + } + ) + + private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = + maybeString match { + case None => default + case Some(dateStr) => SDate(dateStr) + } +} diff --git a/server/src/main/scala/services/exports/QueueExport.scala b/server/src/main/scala/services/api/QueueExport.scala similarity index 54% rename from server/src/main/scala/services/exports/QueueExport.scala rename to server/src/main/scala/services/api/QueueExport.scala index 9255cf6bf..6389309b6 100644 --- a/server/src/main/scala/services/exports/QueueExport.scala +++ b/server/src/main/scala/services/api/QueueExport.scala @@ -1,11 +1,8 @@ -package services.exports +package services.api import akka.stream.Materializer import akka.stream.scaladsl.{Sink, Source} -import controllers.model.RedListCountsJsonFormats.SDateJsonFormat import drt.shared.CrunchApi.{CrunchMinute, MillisSinceEpoch} -import services.exports.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} -import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat} import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.ports.Queues.Queue import uk.gov.homeoffice.drt.ports.Terminals.Terminal @@ -13,36 +10,7 @@ import uk.gov.homeoffice.drt.time.{SDate, SDateLike} import scala.concurrent.{ExecutionContext, Future} -trait QueueExportJsonProtocol extends DefaultJsonProtocol { - implicit object QueueJsonFormat extends RootJsonFormat[Queue] { - override def write(obj: Queue): JsValue = JsString(obj.stringValue) - override def read(json: JsValue): Queue = json match { - case JsString(value) => Queue(value) - case unexpected => throw new Exception(s"Failed to parse Queue. Expected JsString. Got ${unexpected.getClass}") - } - } - - implicit val queueJsonFormat: RootJsonFormat[QueueJson] = jsonFormat3(QueueJson.apply) - - implicit val periodJsonFormat: RootJsonFormat[PeriodJson] = jsonFormat2(PeriodJson.apply) - - implicit object TerminalJsonFormat extends RootJsonFormat[Terminal] { - override def write(obj: Terminal): JsValue = JsString(obj.toString) - - override def read(json: JsValue): Terminal = json match { - case JsString(value) => Terminal(value) - case unexpected => throw new Exception(s"Failed to parse Terminal. Expected JsString. Got ${unexpected.getClass}") - } - } - - implicit val terminalQueuesJsonFormat: RootJsonFormat[TerminalQueuesJson] = jsonFormat2(TerminalQueuesJson.apply) - - implicit val portCodeJsonFormat: RootJsonFormat[PortCode] = jsonFormat1(PortCode.apply) - - implicit val portQueuesJsonFormat: RootJsonFormat[PortQueuesJson] = jsonFormat2(PortQueuesJson.apply) - -} object QueueExport { diff --git a/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala b/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala new file mode 100644 index 000000000..0ea189318 --- /dev/null +++ b/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala @@ -0,0 +1,48 @@ +package services.api.v1.serialisation + +import services.api.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import spray.json.{DefaultJsonProtocol, JsNumber, JsString, JsValue, RootJsonFormat} +import uk.gov.homeoffice.drt.ports.PortCode +import uk.gov.homeoffice.drt.ports.Queues.Queue +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.time.{SDate, SDateLike} + +trait QueueApiJsonProtocol extends DefaultJsonProtocol { + implicit object QueueJsonFormat extends RootJsonFormat[Queue] { + override def write(obj: Queue): JsValue = JsString(obj.stringValue) + + override def read(json: JsValue): Queue = json match { + case JsString(value) => Queue(value) + case unexpected => throw new Exception(s"Failed to parse Queue. Expected JsString. Got ${unexpected.getClass}") + } + } + + implicit val queueJsonFormat: RootJsonFormat[QueueJson] = jsonFormat3(QueueJson.apply) + + implicit object SDateJsonFormat extends RootJsonFormat[SDateLike] { + override def write(obj: SDateLike): JsValue = JsString(obj.toISOString) + + override def read(json: JsValue): SDateLike = json match { + case JsString(value) => SDate(value) + case unexpected => throw new Exception(s"Failed to parse SDate. Expected JsNumber. Got ${unexpected.getClass}") + } + } + + implicit val periodJsonFormat: RootJsonFormat[PeriodJson] = jsonFormat2(PeriodJson.apply) + + implicit object TerminalJsonFormat extends RootJsonFormat[Terminal] { + override def write(obj: Terminal): JsValue = JsString(obj.toString) + + override def read(json: JsValue): Terminal = json match { + case JsString(value) => Terminal(value) + case unexpected => throw new Exception(s"Failed to parse Terminal. Expected JsString. Got ${unexpected.getClass}") + } + } + + implicit val terminalQueuesJsonFormat: RootJsonFormat[TerminalQueuesJson] = jsonFormat2(TerminalQueuesJson.apply) + + implicit val portCodeJsonFormat: RootJsonFormat[PortCode] = jsonFormat(PortCode.apply, "iata") + + implicit val portQueuesJsonFormat: RootJsonFormat[PortQueuesJson] = jsonFormat2(PortQueuesJson.apply) + +} diff --git a/server/src/test/scala/services/exports/QueueExportSpec.scala b/server/src/test/scala/services/exports/QueueExportSpec.scala index a89de9397..c40b8f539 100644 --- a/server/src/test/scala/services/exports/QueueExportSpec.scala +++ b/server/src/test/scala/services/exports/QueueExportSpec.scala @@ -5,7 +5,8 @@ import akka.stream.Materializer import drt.shared.CrunchApi.CrunchMinute import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import services.exports.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import services.api.QueueExport +import services.api.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.ports.Queues.{EGate, EeaDesk, NonEeaDesk} import uk.gov.homeoffice.drt.ports.Terminals.{T1, Terminal} From 23c0bd2c74a5f72f118589a809e445929d1cac67 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Wed, 16 Oct 2024 14:19:53 +0100 Subject: [PATCH 05/13] DRTII-1608 Tweaks --- server/src/main/resources/routes | 3 ++- .../main/scala/services/api/QueueExport.scala | 2 +- .../serialisation/QueueApiJsonProtocol.scala | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/server/src/main/resources/routes b/server/src/main/resources/routes index e9a33b389..3203c1439 100644 --- a/server/src/main/resources/routes +++ b/server/src/main/resources/routes @@ -130,9 +130,10 @@ GET /api/arrivals/:startLocalDate/:endLocalDate/:terminalName + nocsrf GET /api/passengers/:startLocalDate/:endLocalDate controllers.application.SummariesController.exportPassengersByPortForDateRangeApi(startLocalDate: String, endLocalDate: String) GET /api/passengers/:startLocalDate/:endLocalDate/:terminalName controllers.application.SummariesController.exportPassengersByTerminalForDateRangeApi(startLocalDate: String, endLocalDate: String, terminalName: String) -GET /api/v1/passengers controllers.application.api.v1.QueuesApiController.queues() PUT /api/passengers/:localDate controllers.application.SummariesController.populatePassengersForDate(localDate: String) +GET /api/v1/queues controllers.application.api.v1.QueuesApiController.queues() + GET /debug/flights/:persistenceId/:dateString/:messages controllers.application.DebugController.getMessagesForFlightPersistenceIdAtTime(persistenceId: String, dateString: String, messages: Int) POST /email/feedback/:feedback controllers.application.EmailNotificationController.feedBack(feedback: String) # Logging diff --git a/server/src/main/scala/services/api/QueueExport.scala b/server/src/main/scala/services/api/QueueExport.scala index 6389309b6..761b348dc 100644 --- a/server/src/main/scala/services/api/QueueExport.scala +++ b/server/src/main/scala/services/api/QueueExport.scala @@ -14,7 +14,7 @@ import scala.concurrent.{ExecutionContext, Future} object QueueExport { - case class QueueJson(name: Queue, incomingPax: Int, maxWaitMinutes: Int) + case class QueueJson(queue: Queue, incomingPax: Int, maxWaitMinutes: Int) object QueueJson { def apply(cm: CrunchMinute): QueueJson = QueueJson(cm.queue, cm.paxLoad.toInt, cm.waitTime) diff --git a/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala b/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala index 0ea189318..4d7014d76 100644 --- a/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala +++ b/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala @@ -1,7 +1,7 @@ package services.api.v1.serialisation import services.api.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} -import spray.json.{DefaultJsonProtocol, JsNumber, JsString, JsValue, RootJsonFormat} +import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat, enrichAny} import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.ports.Queues.Queue import uk.gov.homeoffice.drt.ports.Terminals.Terminal @@ -9,7 +9,7 @@ import uk.gov.homeoffice.drt.time.{SDate, SDateLike} trait QueueApiJsonProtocol extends DefaultJsonProtocol { implicit object QueueJsonFormat extends RootJsonFormat[Queue] { - override def write(obj: Queue): JsValue = JsString(obj.stringValue) + override def write(obj: Queue): JsValue = obj.stringValue.toJson override def read(json: JsValue): Queue = json match { case JsString(value) => Queue(value) @@ -20,7 +20,7 @@ trait QueueApiJsonProtocol extends DefaultJsonProtocol { implicit val queueJsonFormat: RootJsonFormat[QueueJson] = jsonFormat3(QueueJson.apply) implicit object SDateJsonFormat extends RootJsonFormat[SDateLike] { - override def write(obj: SDateLike): JsValue = JsString(obj.toISOString) + override def write(obj: SDateLike): JsValue = obj.toISOString.toJson override def read(json: JsValue): SDateLike = json match { case JsString(value) => SDate(value) @@ -31,7 +31,7 @@ trait QueueApiJsonProtocol extends DefaultJsonProtocol { implicit val periodJsonFormat: RootJsonFormat[PeriodJson] = jsonFormat2(PeriodJson.apply) implicit object TerminalJsonFormat extends RootJsonFormat[Terminal] { - override def write(obj: Terminal): JsValue = JsString(obj.toString) + override def write(obj: Terminal): JsValue = obj.toString.toJson override def read(json: JsValue): Terminal = json match { case JsString(value) => Terminal(value) @@ -41,7 +41,15 @@ trait QueueApiJsonProtocol extends DefaultJsonProtocol { implicit val terminalQueuesJsonFormat: RootJsonFormat[TerminalQueuesJson] = jsonFormat2(TerminalQueuesJson.apply) - implicit val portCodeJsonFormat: RootJsonFormat[PortCode] = jsonFormat(PortCode.apply, "iata") + implicit object PortCodeJsonFormat extends RootJsonFormat[PortCode] { + override def write(obj: PortCode): JsValue = obj.iata.toJson + + override def read(json: JsValue): PortCode = json match { + case JsString(value) => PortCode(value) + case unexpected => throw new Exception(s"Failed to parse Terminal. Expected JsString. Got ${unexpected.getClass}") + } + } + implicit val portQueuesJsonFormat: RootJsonFormat[PortQueuesJson] = jsonFormat2(PortQueuesJson.apply) From 71d281208c286beea3965f7042ab01286ec62aa2 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Thu, 17 Oct 2024 09:33:25 +0100 Subject: [PATCH 06/13] DRTII-1608 Add v1 flights api --- flights-export.json | 20 +-- project/Settings.scala | 2 +- server/src/main/resources/routes | 1 + .../src/main/scala/api/ApiResponseBody.scala | 9 -- server/src/main/scala/api/KeyCloakAuth.scala | 132 ------------------ .../main/scala/controllers/Application.scala | 15 +- .../application/ImportsController.scala | 8 +- .../api/v1/FlightsApiController.scala | 64 +++++++++ .../api/v1/QueuesApiController.scala | 2 +- .../scala/services/api/v1/FlightExport.scala | 61 ++++++++ .../services/api/{ => v1}/QueueExport.scala | 2 +- .../serialisation/FlightApiJsonProtocol.scala | 59 ++++++++ .../serialisation/QueueApiJsonProtocol.scala | 2 +- .../drt/crunchsystem/DrtSystemInterface.scala | 2 +- .../services/exports/QueueExportSpec.scala | 4 +- 15 files changed, 216 insertions(+), 167 deletions(-) delete mode 100644 server/src/main/scala/api/ApiResponseBody.scala delete mode 100644 server/src/main/scala/api/KeyCloakAuth.scala create mode 100644 server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala create mode 100644 server/src/main/scala/services/api/v1/FlightExport.scala rename server/src/main/scala/services/api/{ => v1}/QueueExport.scala (98%) create mode 100644 server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala diff --git a/flights-export.json b/flights-export.json index b80c5029c..d53d43d43 100644 --- a/flights-export.json +++ b/flights-export.json @@ -1,6 +1,6 @@ { - "start-time": "2024-10-15T12:07", - "end-time": "2024-10-15T14:06", + "startTime": "2024-10-15T12:07", + "endTime": "2024-10-15T14:06", "ports": [ { "name": "LHR", @@ -11,18 +11,18 @@ { "code": "BA0001", "origin": "JFK", - "scheduled-time": "2024-10-15T12:05", - "estimated-pcp-arrival-start-time": "2024-10-15T12:15", - "estimated-pcp-arrival-end-time": "2024-10-15T12:19", - "estimated-passenger-count": 100 + "scheduledTime": "2024-10-15T12:05", + "estimatedPcpArrivalStartTime": "2024-10-15T12:15", + "estimatedPcpArrivalEndTime": "2024-10-15T12:19", + "estimatedPassengerCount": 100 }, { "code": "BA0002", "origin": "CDG", - "scheduled-time": "2024-10-15T12:20", - "estimated-pcp-arrival-start-time": "2024-10-15T12:32", - "estimated-pcp-arrival-end-time": "2024-10-15T12:33", - "estimated-passenger-count": 50 + "scheduledTime": "2024-10-15T12:20", + "estimatedPcpArrivalStartTime": "2024-10-15T12:32", + "estimatedPcpArrivalEndTime": "2024-10-15T12:33", + "estimatedPassengerCount": 50 } ] } diff --git a/project/Settings.scala b/project/Settings.scala index bfb5c53d7..e493e0e4b 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -25,7 +25,7 @@ object Settings { /** Declare global dependency versions here to avoid mismatches in multi part dependencies */ //noinspection ScalaStyle object versions { - val drtLib = "v898" + val drtLib = "v903" val scala = "2.13.12" val scalaDom = "2.8.0" diff --git a/server/src/main/resources/routes b/server/src/main/resources/routes index 3203c1439..2c18ce557 100644 --- a/server/src/main/resources/routes +++ b/server/src/main/resources/routes @@ -133,6 +133,7 @@ GET /api/passengers/:startLocalDate/:endLocalDate/:terminalName PUT /api/passengers/:localDate controllers.application.SummariesController.populatePassengersForDate(localDate: String) GET /api/v1/queues controllers.application.api.v1.QueuesApiController.queues() +GET /api/v1/flights controllers.application.api.v1.FlightsApiController.flights() GET /debug/flights/:persistenceId/:dateString/:messages controllers.application.DebugController.getMessagesForFlightPersistenceIdAtTime(persistenceId: String, dateString: String, messages: Int) POST /email/feedback/:feedback controllers.application.EmailNotificationController.feedBack(feedback: String) diff --git a/server/src/main/scala/api/ApiResponseBody.scala b/server/src/main/scala/api/ApiResponseBody.scala deleted file mode 100644 index 3c5ead221..000000000 --- a/server/src/main/scala/api/ApiResponseBody.scala +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import play.api.libs.json.Json._ -import play.api.libs.json.OWrites - -case class ApiResponseBody(message: String) -object ApiResponseBody { - implicit val w: OWrites[ApiResponseBody] = writes[ApiResponseBody] -} diff --git a/server/src/main/scala/api/KeyCloakAuth.scala b/server/src/main/scala/api/KeyCloakAuth.scala deleted file mode 100644 index 6eb894050..000000000 --- a/server/src/main/scala/api/KeyCloakAuth.scala +++ /dev/null @@ -1,132 +0,0 @@ -package api - -import akka.actor.ActorSystem -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.Accept -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.Materializer -import drt.http.WithSendAndReceive -import org.slf4j.{Logger, LoggerFactory} -import spray.json.{DefaultJsonProtocol, JsNumber, JsObject, JsString, JsValue, RootJsonFormat} - -import scala.concurrent.Future - -abstract case class KeyCloakAuth(tokenUrl: String, clientId: String, clientSecret: String)(implicit val system: ActorSystem, mat: Materializer) - extends WithSendAndReceive with KeyCloakAuthTokenParserProtocol { - - import system.dispatcher - - val log: Logger = LoggerFactory.getLogger(getClass) - - def formData(username: String, password: String, clientId: String, clientSecret: String) = FormData(Map( - "username" -> username, - "password" -> password, - "client_id" -> clientId, - "client_secret" -> clientSecret, - "grant_type" -> "password" - )) - - def getToken(username: String, password: String): Future[KeyCloakAuthResponse] = { - val request = HttpRequest( - method = HttpMethods.POST, - uri = Uri(tokenUrl), - headers = List(Accept(MediaTypes.`application/json`)), - entity = formData(username, password, clientId, clientSecret).toEntity) - - val requestWithHeaders = request.addHeader(Accept(MediaTypes.`application/json`)) - - sendAndReceive(requestWithHeaders).flatMap { r => - Unmarshal(r).to[KeyCloakAuthResponse] - } - } -} - -sealed trait KeyCloakAuthResponse - -case class KeyCloakAuthToken(accessToken: String, - expiresIn: Int, - refreshExpiresIn: Int, - refreshToken: String, - tokenType: String, - notBeforePolicy: Int, - sessionState: String, - scope: String) extends KeyCloakAuthResponse - -case class KeyCloakAuthError(error: String, errorDescription: String) extends KeyCloakAuthResponse - -object KeyCloakAuthTokenParserProtocol extends KeyCloakAuthTokenParserProtocol - - -trait KeyCloakAuthTokenParserProtocol extends SprayJsonSupport with DefaultJsonProtocol { - implicit val responseFormat: RootJsonFormat[KeyCloakAuthResponse] = new RootJsonFormat[KeyCloakAuthResponse] { - override def write(response: KeyCloakAuthResponse): JsValue = response match { - case KeyCloakAuthToken(token, expires, _, _, tokenType, _, _, _) => JsObject( - "access_token" -> JsString(token), - "expires_in" -> JsNumber(expires), - "token_type" -> JsString(tokenType) - ) - case KeyCloakAuthError(error, desc) => JsObject( - "error" -> JsString(error), - "error_description" -> JsString(desc) - ) - } - - override def read(json: JsValue): KeyCloakAuthResponse = json match { - case JsObject(fields) if fields.contains("access_token") => - KeyCloakAuthToken( - fields.get("access_token").map(_.convertTo[String]).getOrElse(""), - fields.get("expires_in").map(_.convertTo[Int]).getOrElse(0), - fields.get("refresh_expires_in").map(_.convertTo[Int]).getOrElse(0), - fields.get("refresh_token").map(_.convertTo[String]).getOrElse(""), - fields.get("token_type").map(_.convertTo[String]).getOrElse(""), - fields.get("not-before-policy").map(_.convertTo[Int]).getOrElse(0), - fields.get("session_state").map(_.convertTo[String]).getOrElse(""), - fields.get("scope").map(_.convertTo[String]).getOrElse("") - ) - case JsObject(fields) => - KeyCloakAuthError( - fields.get("error").map(_.convertTo[String]).getOrElse(""), - fields.get("error_description").map(_.convertTo[String]).getOrElse("") - ) - } - } - - implicit val tokenFormat: RootJsonFormat[KeyCloakAuthToken] = new RootJsonFormat[KeyCloakAuthToken] { - override def write(token: KeyCloakAuthToken): JsValue = JsObject( - "access_token" -> JsString(token.accessToken), - "expires_in" -> JsNumber(token.expiresIn), - "token_type" -> JsString(token.tokenType) - ) - - override def read(json: JsValue): KeyCloakAuthToken = json match { - case JsObject(fields) if fields.contains("access_token") => - KeyCloakAuthToken( - fields.get("access_token").map(_.convertTo[String]).getOrElse(""), - fields.get("expires_in").map(_.convertTo[Int]).getOrElse(0), - fields.get("refresh_expires_in").map(_.convertTo[Int]).getOrElse(0), - fields.get("refresh_token").map(_.convertTo[String]).getOrElse(""), - fields.get("token_type").map(_.convertTo[String]).getOrElse(""), - fields.get("not-before-policy").map(_.convertTo[Int]).getOrElse(0), - fields.get("session_state").map(_.convertTo[String]).getOrElse(""), - fields.get("scope").map(_.convertTo[String]).getOrElse("") - ) - } - } - - implicit val errorFormat: RootJsonFormat[KeyCloakAuthError] = new RootJsonFormat[KeyCloakAuthError] { - override def write(error: KeyCloakAuthError): JsValue = JsObject( - "error" -> JsString(error.error), - "error_description" -> JsString(error.errorDescription) - ) - - override def read(json: JsValue): KeyCloakAuthError = json match { - case JsObject(fields) => - KeyCloakAuthError( - fields.get("error").map(_.convertTo[String]).getOrElse(""), - fields.get("error_description").map(_.convertTo[String]).getOrElse("") - ) - case _ => KeyCloakAuthError("", "") - } - } -} diff --git a/server/src/main/scala/controllers/Application.scala b/server/src/main/scala/controllers/Application.scala index e901e085e..6aea01dae 100644 --- a/server/src/main/scala/controllers/Application.scala +++ b/server/src/main/scala/controllers/Application.scala @@ -1,22 +1,23 @@ package controllers import akka.event.Logging -import api._ +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} import buildinfo.BuildInfo import com.google.inject.Inject import com.typesafe.config.ConfigFactory import controllers.application._ -import drt.http.ProdSendAndReceive +import spray.json.enrichAny import drt.shared.DrtPortConfigs import org.joda.time.chrono.ISOChronology import play.api.mvc._ import play.api.{Configuration, Environment} import services.{ActorResponseTimeHealthCheck, FeedsHealthCheck, HealthChecker} import slickdb._ -import spray.json.enrichAny import uk.gov.homeoffice.drt.auth.Roles.BorderForceStaff import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.db.dao.{IABFeatureDao, IUserFeedbackDao} +import uk.gov.homeoffice.drt.keycloak.{KeyCloakAuth, KeyCloakAuthError, KeyCloakAuthResponse, KeyCloakAuthToken, KeyCloakAuthTokenParserProtocol} import uk.gov.homeoffice.drt.ports._ import uk.gov.homeoffice.drt.time.TimeZoneHelper.europeLondonTimeZone import uk.gov.homeoffice.drt.time.{MilliTimes, SDate, SDateLike} @@ -73,7 +74,7 @@ trait ABFeatureProviderLike { } class Application @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface)(implicit environment: Environment) - extends AuthController(cc, ctrl) { + extends AuthController(cc, ctrl) with KeyCloakAuthTokenParserProtocol { val googleTrackingCode: String = config.get[String]("googleTrackingCode") @@ -193,7 +194,6 @@ class Application @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface)( def viewedFeatureGuideIds: Action[AnyContent] = authByRole(BorderForceStaff) { Action.async { implicit request => - import spray.json.DefaultJsonProtocol.{StringJsonFormat, immSeqFormat} val userEmail = request.headers.get("X-Forwarded-Email").getOrElse("Unknown") ctrl.featureGuideViewService.featureViewed(userEmail).map(a => Ok(a.toJson.toString())) } @@ -252,7 +252,7 @@ class Application @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface)( val clientSecretOption = config.getOptional[String]("key-cloak.client_secret") val usernameOption = postStringValOrElse("username") val passwordOption = postStringValOrElse("password") - import KeyCloakAuthTokenParserProtocol._ +// import KeyCloakAuthTokenParserProtocol._ import spray.json._ def tokenToHttpResponse(username: String)(token: KeyCloakAuthResponse): Result = token match { @@ -274,7 +274,8 @@ class Application @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface)( clientSecret <- clientSecretOption } yield (usernameOption, passwordOption) match { case (Some(username), Some(password)) => - val authClient = new KeyCloakAuth(tokenUrl, clientId, clientSecret) with ProdSendAndReceive + val requestToEventualResponse: HttpRequest => Future[HttpResponse] = request => Http().singleRequest(request) + val authClient = KeyCloakAuth(tokenUrl, clientId, clientSecret, requestToEventualResponse) authClient.getToken(username, password).map(tokenToHttpResponse(username)) case _ => log.info(s"Invalid post fields for api login.") diff --git a/server/src/main/scala/controllers/application/ImportsController.scala b/server/src/main/scala/controllers/application/ImportsController.scala index b0a771d8f..af0a5529c 100644 --- a/server/src/main/scala/controllers/application/ImportsController.scala +++ b/server/src/main/scala/controllers/application/ImportsController.scala @@ -3,17 +3,16 @@ package controllers.application import actors.persistent.nebo.NeboArrivalActor import akka.actor.ActorRef import akka.pattern.ask -import api.ApiResponseBody import com.google.inject.Inject import controllers.model.RedListCounts import controllers.model.RedListCountsJsonFormats._ import drt.server.feeds.StoreFeedImportArrivals import drt.server.feeds.lhr.forecast.LHRForecastCSVExtractor import drt.server.feeds.stn.STNForecastXLSExtractor -import drt.shared.FlightsApi.Flights import drt.shared.{NeboArrivals, RedListPassengers} import play.api.libs.Files import play.api.libs.json.Json._ +import play.api.libs.json.OWrites import play.api.mvc._ import spray.json._ import uk.gov.homeoffice.drt.auth.Roles.{NeboUpload, PortFeedUpload} @@ -26,6 +25,11 @@ import scala.concurrent.Future import scala.util.Try +case class ApiResponseBody(message: String) +object ApiResponseBody { + implicit val w: OWrites[ApiResponseBody] = writes[ApiResponseBody] +} + class ImportsController@Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) { def feedImportRedListCounts: Action[AnyContent] = authByRole(NeboUpload) { diff --git a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala new file mode 100644 index 000000000..df3f3d0e2 --- /dev/null +++ b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala @@ -0,0 +1,64 @@ +package controllers.application.api.v1 + +import actors.PartitionedPortStateActor.GetFlightsForTerminals +import akka.NotUsed +import akka.pattern.ask +import akka.stream.scaladsl.{Sink, Source} +import com.google.inject.Inject +import controllers.application.AuthController +import play.api.mvc._ +import services.api.v1.FlightExport +import services.api.v1.serialisation.FlightApiJsonProtocol +import spray.json.enrichAny +import uk.gov.homeoffice.drt.arrivals.{Arrival, FlightsWithSplits} +import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface +import uk.gov.homeoffice.drt.ports.FeedSource +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.time.{SDate, SDateLike, UtcDate} + +import scala.concurrent.Future +import scala.util.Try + + +class FlightsApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) with FlightApiJsonProtocol { + implicit val pfso: List[FeedSource] = ctrl.paxFeedSourceOrder + + private val flightTotalsForGranularity: (SDateLike, SDateLike, Terminal) => Future[Seq[Arrival]] = + (start, end, terminal) => { + val request = GetFlightsForTerminals(start.millisSinceEpoch, end.millisSinceEpoch, Seq(terminal)) + ctrl.actorService.flightsRouterActor.ask(request).mapTo[Source[(UtcDate, FlightsWithSplits), NotUsed]] + .flatMap(_ + .map { case (_, FlightsWithSplits(flights)) => + flights.values.map(_.apiFlight).toSeq + .sortBy(a => Try(a.pcpRange(pfso).min).getOrElse(a.Scheduled)) + } + .runWith(Sink.fold(Seq[Arrival]())(_ ++ _)) + ) + } + + private val flightExport: (SDateLike, SDateLike) => Future[FlightExport.PortFlightsJson] = + FlightExport(flightTotalsForGranularity, ctrl.airportConfig.terminals, ctrl.airportConfig.portCode) + + def flights(): Action[AnyContent] = + auth( + Action.async { + request => + val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) + val end = parseOptionalEndDate(request.getQueryString("end"), SDate.now()) + if (start > end) { + throw new Exception("Start date must be before end date") + } + + log.info(s"\n\nGetting flights for ${start.toISOString} -> ${end.toISOString}\n\n") + + flightExport(start, end) + .map(r => Ok(r.toJson.compactPrint)) + } + ) + + private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = + maybeString match { + case None => default + case Some(dateStr) => SDate(dateStr) + } +} diff --git a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala index e2897231c..76e918fb3 100644 --- a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala @@ -7,7 +7,7 @@ import controllers.application.AuthController import drt.shared.CrunchApi.{CrunchMinute, MinutesContainer} import drt.shared.{CrunchApi, TQM} import play.api.mvc._ -import services.api.QueueExport +import services.api.v1.QueueExport import services.api.v1.serialisation.QueueApiJsonProtocol import spray.json.enrichAny import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface diff --git a/server/src/main/scala/services/api/v1/FlightExport.scala b/server/src/main/scala/services/api/v1/FlightExport.scala new file mode 100644 index 000000000..b9914b101 --- /dev/null +++ b/server/src/main/scala/services/api/v1/FlightExport.scala @@ -0,0 +1,61 @@ +package services.api.v1 + +import akka.stream.Materializer +import akka.stream.scaladsl.{Sink, Source} +import uk.gov.homeoffice.drt.arrivals.Arrival +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.ports.{FeedSource, PortCode} +import uk.gov.homeoffice.drt.time.SDateLike + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try + + +object FlightExport { + + case class FlightJson(code: String, + originPort: String, + scheduledTime: Long, + estimatedPcpStartTime: Option[Long], + estimatedPcpEndTime: Option[Long], + estimatedPaxCount: Option[Int], + status: String, + ) + + object FlightJson { + def apply(ar: Arrival) + (implicit sourceOrderPreference: List[FeedSource]): FlightJson = FlightJson( + ar.flightCodeString, + ar.Origin.iata, + ar.Scheduled, + Try(ar.pcpRange(sourceOrderPreference).min).toOption, + Try(ar.pcpRange(sourceOrderPreference).max).toOption, + ar.bestPcpPaxEstimate(sourceOrderPreference), + ar.Status.description, + ) + } + + case class TerminalFlightsJson(terminal: Terminal, flights: Iterable[FlightJson]) + + case class PortFlightsJson(portCode: PortCode, terminals: Iterable[TerminalFlightsJson]) + + def apply(minutesSource: (SDateLike, SDateLike, Terminal) => Future[Seq[Arrival]], + terminals: Iterable[Terminal], + portCode: PortCode, + ) + (implicit mat: Materializer, ec: ExecutionContext, sourceOrderPreference: List[FeedSource]): (SDateLike, SDateLike) => Future[PortFlightsJson] = + (start, end) => { + Source(terminals.toSeq) + .mapAsync(terminals.size) { terminal => + minutesSource(start, end, terminal).map(_.map(FlightJson.apply(_))) + } + .runWith(Sink.seq) + .map { + terminalPeriods => + val terminalFlights = terminals.zip(terminalPeriods).map { + case (terminal, periods) => TerminalFlightsJson(terminal, periods) + } + PortFlightsJson(portCode, terminalFlights) + } + } +} diff --git a/server/src/main/scala/services/api/QueueExport.scala b/server/src/main/scala/services/api/v1/QueueExport.scala similarity index 98% rename from server/src/main/scala/services/api/QueueExport.scala rename to server/src/main/scala/services/api/v1/QueueExport.scala index 761b348dc..14cc26714 100644 --- a/server/src/main/scala/services/api/QueueExport.scala +++ b/server/src/main/scala/services/api/v1/QueueExport.scala @@ -1,4 +1,4 @@ -package services.api +package services.api.v1 import akka.stream.Materializer import akka.stream.scaladsl.{Sink, Source} diff --git a/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala b/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala new file mode 100644 index 000000000..60c069362 --- /dev/null +++ b/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala @@ -0,0 +1,59 @@ +package services.api.v1.serialisation + +import services.api.v1.FlightExport.{FlightJson, PortFlightsJson, TerminalFlightsJson} +import spray.json.{DefaultJsonProtocol, JsObject, JsString, JsValue, RootJsonFormat, enrichAny} +import uk.gov.homeoffice.drt.ports.PortCode +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.time.SDate + +trait FlightApiJsonProtocol extends DefaultJsonProtocol { + implicit object FlightJsonJsonFormat extends RootJsonFormat[FlightJson] { + override def write(obj: FlightJson): JsValue = JsObject( + "code" -> obj.code.toJson, + "originPort" -> obj.originPort.toJson, + "scheduledTime" -> SDate(obj.scheduledTime).toISOString.toJson, + "estimatedPcpStartTime" -> obj.estimatedPcpStartTime.map(SDate(_).toISOString).toJson, + "estimatedPcpEndTime" -> obj.estimatedPcpEndTime.map(SDate(_).toISOString).toJson, + "estimatedPaxCount" -> obj.estimatedPaxCount.toJson, + "status" -> obj.status.toJson + ) + + override def read(json: JsValue): FlightJson = json match { + case JsObject(fields) => FlightJson( + fields.get("code").map(_.convertTo[String]).getOrElse(""), + fields.get("originPort").map(_.convertTo[String]).getOrElse(""), + fields.get("scheduledTime").map(_.convertTo[Long]).getOrElse(0L), + fields.get("estimatedPcpStartTime").map(_.convertTo[Long]), + fields.get("estimatedPcpEndTime").map(_.convertTo[Long]), + fields.get("estimatedPaxCount").map(_.convertTo[Int]), + fields.get("status").map(_.convertTo[String]).getOrElse(""), + ) + case unexpected => throw new Exception(s"Failed to parse FlightJson. Expected JsString. Got ${unexpected.getClass}") + } + } + + implicit val flightJsonFormat: RootJsonFormat[FlightJson] = jsonFormat7(FlightJson.apply) + + implicit object TerminalJsonFormat extends RootJsonFormat[Terminal] { + override def write(obj: Terminal): JsValue = obj.toString.toJson + + override def read(json: JsValue): Terminal = json match { + case JsString(value) => Terminal(value) + case unexpected => throw new Exception(s"Failed to parse Terminal. Expected JsString. Got ${unexpected.getClass}") + } + } + + implicit val terminalFlightsJsonFormat: RootJsonFormat[TerminalFlightsJson] = jsonFormat2(TerminalFlightsJson.apply) + + implicit object PortCodeJsonFormat extends RootJsonFormat[PortCode] { + override def write(obj: PortCode): JsValue = obj.iata.toJson + + override def read(json: JsValue): PortCode = json match { + case JsString(value) => PortCode(value) + case unexpected => throw new Exception(s"Failed to parse Terminal. Expected JsString. Got ${unexpected.getClass}") + } + } + + + implicit val portFlightsJsonFormat: RootJsonFormat[PortFlightsJson] = jsonFormat2(PortFlightsJson.apply) +} diff --git a/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala b/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala index 4d7014d76..1cd739e69 100644 --- a/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala +++ b/server/src/main/scala/services/api/v1/serialisation/QueueApiJsonProtocol.scala @@ -1,6 +1,6 @@ package services.api.v1.serialisation -import services.api.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import services.api.v1.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat, enrichAny} import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.ports.Queues.Queue diff --git a/server/src/main/scala/uk/gov/homeoffice/drt/crunchsystem/DrtSystemInterface.scala b/server/src/main/scala/uk/gov/homeoffice/drt/crunchsystem/DrtSystemInterface.scala index 7f226a712..2acfe6359 100644 --- a/server/src/main/scala/uk/gov/homeoffice/drt/crunchsystem/DrtSystemInterface.scala +++ b/server/src/main/scala/uk/gov/homeoffice/drt/crunchsystem/DrtSystemInterface.scala @@ -52,7 +52,7 @@ trait DrtSystemInterface extends UserRoleProviderLike val now: () => SDateLike - val paxFeedSourceOrder: List[FeedSource] = if (params.usePassengerPredictions) List( + implicit val paxFeedSourceOrder: List[FeedSource] = if (params.usePassengerPredictions) List( ScenarioSimulationSource, LiveFeedSource, ApiFeedSource, diff --git a/server/src/test/scala/services/exports/QueueExportSpec.scala b/server/src/test/scala/services/exports/QueueExportSpec.scala index c40b8f539..b755770f5 100644 --- a/server/src/test/scala/services/exports/QueueExportSpec.scala +++ b/server/src/test/scala/services/exports/QueueExportSpec.scala @@ -5,8 +5,8 @@ import akka.stream.Materializer import drt.shared.CrunchApi.CrunchMinute import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import services.api.QueueExport -import services.api.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import services.api.v1.QueueExport.{PeriodJson, PortQueuesJson, QueueJson, TerminalQueuesJson} +import services.api.v1.QueueExport import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.ports.Queues.{EGate, EeaDesk, NonEeaDesk} import uk.gov.homeoffice.drt.ports.Terminals.{T1, Terminal} From faf755be49f6cb1bb91ff7fe407c79052808d786 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Thu, 17 Oct 2024 10:08:16 +0100 Subject: [PATCH 07/13] DRTII-1608 Tidying --- .../api/v1/FlightsApiController.scala | 15 ++++++++---- .../api/v1/QueuesApiController.scala | 5 +--- .../serialisation/FlightApiJsonProtocol.scala | 23 +++++++++++-------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala index df3f3d0e2..95c9ceac7 100644 --- a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala @@ -26,10 +26,17 @@ class FlightsApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemIn private val flightTotalsForGranularity: (SDateLike, SDateLike, Terminal) => Future[Seq[Arrival]] = (start, end, terminal) => { val request = GetFlightsForTerminals(start.millisSinceEpoch, end.millisSinceEpoch, Seq(terminal)) + ctrl.actorService.flightsRouterActor.ask(request).mapTo[Source[(UtcDate, FlightsWithSplits), NotUsed]] .flatMap(_ .map { case (_, FlightsWithSplits(flights)) => - flights.values.map(_.apiFlight).toSeq + flights.values + .filter { fws => + val startTime = Try(fws.apiFlight.pcpRange(pfso).min).getOrElse(fws.apiFlight.Scheduled) + val endTime = Try(fws.apiFlight.pcpRange(pfso).max).getOrElse(fws.apiFlight.Scheduled) + startTime >= start.millisSinceEpoch && endTime <= end.millisSinceEpoch + } + .map(_.apiFlight).toSeq .sortBy(a => Try(a.pcpRange(pfso).min).getOrElse(a.Scheduled)) } .runWith(Sink.fold(Seq[Arrival]())(_ ++ _)) @@ -45,14 +52,12 @@ class FlightsApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemIn request => val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) val end = parseOptionalEndDate(request.getQueryString("end"), SDate.now()) + if (start > end) { throw new Exception("Start date must be before end date") } - log.info(s"\n\nGetting flights for ${start.toISOString} -> ${end.toISOString}\n\n") - - flightExport(start, end) - .map(r => Ok(r.toJson.compactPrint)) + flightExport(start, end).map(r => Ok(r.toJson.compactPrint)) } ) diff --git a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala index 76e918fb3..ddba6ee63 100644 --- a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala @@ -44,10 +44,7 @@ class QueuesApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt } val periodMinutes = request.getQueryString("period-minutes").map(_.toInt).getOrElse(15) - log.info(s"\n\nGetting queues for ${start.toISOString} -> ${end.toISOString} every $periodMinutes minutes\n\n") - - queueExport(start, end, periodMinutes) - .map(r => Ok(r.toJson.compactPrint)) + queueExport(start, end, periodMinutes).map(r => Ok(r.toJson.compactPrint)) } ) diff --git a/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala b/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala index 60c069362..027774541 100644 --- a/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala +++ b/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala @@ -8,15 +8,18 @@ import uk.gov.homeoffice.drt.time.SDate trait FlightApiJsonProtocol extends DefaultJsonProtocol { implicit object FlightJsonJsonFormat extends RootJsonFormat[FlightJson] { - override def write(obj: FlightJson): JsValue = JsObject( - "code" -> obj.code.toJson, - "originPort" -> obj.originPort.toJson, - "scheduledTime" -> SDate(obj.scheduledTime).toISOString.toJson, - "estimatedPcpStartTime" -> obj.estimatedPcpStartTime.map(SDate(_).toISOString).toJson, - "estimatedPcpEndTime" -> obj.estimatedPcpEndTime.map(SDate(_).toISOString).toJson, - "estimatedPaxCount" -> obj.estimatedPaxCount.toJson, - "status" -> obj.status.toJson - ) + override def write(obj: FlightJson): JsValue = { + val maybePax = obj.estimatedPaxCount.filter(_ > 0) + JsObject( + "code" -> obj.code.toJson, + "originPort" -> obj.originPort.toJson, + "scheduledTime" -> SDate(obj.scheduledTime).toISOString.toJson, + "estimatedPcpStartTime" -> maybePax.flatMap(_ => obj.estimatedPcpStartTime.map(SDate(_).toISOString)).toJson, + "estimatedPcpEndTime" -> maybePax.flatMap(_ => obj.estimatedPcpEndTime.map(SDate(_).toISOString)).toJson, + "estimatedPcpPaxCount" -> obj.estimatedPaxCount.toJson, + "status" -> obj.status.toJson + ) + } override def read(json: JsValue): FlightJson = json match { case JsObject(fields) => FlightJson( @@ -25,7 +28,7 @@ trait FlightApiJsonProtocol extends DefaultJsonProtocol { fields.get("scheduledTime").map(_.convertTo[Long]).getOrElse(0L), fields.get("estimatedPcpStartTime").map(_.convertTo[Long]), fields.get("estimatedPcpEndTime").map(_.convertTo[Long]), - fields.get("estimatedPaxCount").map(_.convertTo[Int]), + fields.get("estimatedPcpPaxCount").map(_.convertTo[Int]), fields.get("status").map(_.convertTo[String]).getOrElse(""), ) case unexpected => throw new Exception(s"Failed to parse FlightJson. Expected JsString. Got ${unexpected.getClass}") From d69b2418287c652fbd797a2d324c9b53fbdffcaf Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Sat, 19 Oct 2024 11:15:00 +0100 Subject: [PATCH 08/13] DRTII-1608 Tests around queue & flight exports --- .../scala/services/api/v1/FlightExport.scala | 6 +- .../scala/services/api/v1/QueueExport.scala | 13 +++-- .../src/test/scala/api/KeyCloakAuthSpec.scala | 56 +++++++++---------- .../services/exports/FlightExportSpec.scala | 54 ++++++++++++++++++ .../services/exports/QueueExportSpec.scala | 37 +++++++----- 5 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 server/src/test/scala/services/exports/FlightExportSpec.scala diff --git a/server/src/main/scala/services/api/v1/FlightExport.scala b/server/src/main/scala/services/api/v1/FlightExport.scala index b9914b101..c39afca80 100644 --- a/server/src/main/scala/services/api/v1/FlightExport.scala +++ b/server/src/main/scala/services/api/v1/FlightExport.scala @@ -1,5 +1,6 @@ package services.api.v1 +import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.{Sink, Source} import uk.gov.homeoffice.drt.arrivals.Arrival @@ -47,7 +48,10 @@ object FlightExport { (start, end) => { Source(terminals.toSeq) .mapAsync(terminals.size) { terminal => - minutesSource(start, end, terminal).map(_.map(FlightJson.apply(_))) + minutesSource(start, end, terminal).map { + _.filter(_.hasPcpDuring(start, end, sourceOrderPreference)) + .map(FlightJson.apply(_)) + } } .runWith(Sink.seq) .map { diff --git a/server/src/main/scala/services/api/v1/QueueExport.scala b/server/src/main/scala/services/api/v1/QueueExport.scala index 14cc26714..15e6999eb 100644 --- a/server/src/main/scala/services/api/v1/QueueExport.scala +++ b/server/src/main/scala/services/api/v1/QueueExport.scala @@ -11,7 +11,6 @@ import uk.gov.homeoffice.drt.time.{SDate, SDateLike} import scala.concurrent.{ExecutionContext, Future} - object QueueExport { case class QueueJson(queue: Queue, incomingPax: Int, maxWaitMinutes: Int) @@ -36,9 +35,15 @@ object QueueExport { .mapAsync(terminals.size) { terminal => minutesSource(start, end, terminal, periodMinutes) .map { queueTotals: Iterable[(MillisSinceEpoch, Seq[CrunchMinute])] => - queueTotals.map { case (slotTime, queues) => - PeriodJson(SDate(slotTime), queues.map(QueueJson.apply)) - } + queueTotals + .filter { + case (slotTime, _) => + println(s"${start.toISOString} <= ${SDate(slotTime).toISOString} < ${end.toISOString}") + start.millisSinceEpoch <= slotTime && slotTime < end.millisSinceEpoch + } + .map { case (slotTime, queues) => + PeriodJson(SDate(slotTime), queues.map(QueueJson.apply)) + } } } .runWith(Sink.seq) diff --git a/server/src/test/scala/api/KeyCloakAuthSpec.scala b/server/src/test/scala/api/KeyCloakAuthSpec.scala index e5854af5a..4e61f4415 100644 --- a/server/src/test/scala/api/KeyCloakAuthSpec.scala +++ b/server/src/test/scala/api/KeyCloakAuthSpec.scala @@ -2,34 +2,34 @@ package api import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpRequest, HttpResponse} import services.crunch.CrunchTestLike +import uk.gov.homeoffice.drt.keycloak.{KeyCloakAuth, KeyCloakAuthError, KeyCloakAuthToken, KeyCloakAuthTokenParserProtocol} import scala.concurrent.duration._ import scala.concurrent.{Await, Future} -class KeyCloakAuthSpec extends CrunchTestLike { +class KeyCloakAuthSpec extends CrunchTestLike with KeyCloakAuthTokenParserProtocol { val keyCloakUrl = "https://keycloak" val tokenResponseJson: String = s"""{ - | "access_token": "token", - | "expires_in": 86400, - | "refresh_expires_in": 86400, - | "refresh_token": "refresh token", - | "token_type": "bearer", - | "not-before-policy": 0, - | "session_state": "session", - | "scope": "profile email" - |}""".stripMargin + | "access_token": "token", + | "expires_in": 86400, + | "refresh_expires_in": 86400, + | "refresh_token": "refresh token", + | "token_type": "bearer", + | "not-before-policy": 0, + | "session_state": "session", + | "scope": "profile email" + |}""".stripMargin "When parsing keycloak JSON token I should get back a case class representation of the token" >> { - import KeyCloakAuthTokenParserProtocol._ import spray.json._ val expected = KeyCloakAuthToken( - "token", + "token", 86400, 86400, "refresh token", @@ -46,11 +46,9 @@ class KeyCloakAuthSpec extends CrunchTestLike { "When logging into Keycloak with a correct username and password then I should get a token back" >> { - val auth = new KeyCloakAuth("tokenurl", "clientId", "client secret") { - def sendAndReceive: HttpRequest => Future[HttpResponse] = (_: HttpRequest) => { - Future(HttpResponse().withEntity(HttpEntity(ContentTypes.`application/json`, tokenResponseJson))) - } - } + val sendHttpRequest: HttpRequest => Future[HttpResponse] = + _ => Future.successful(HttpResponse().withEntity(HttpEntity(ContentTypes.`application/json`, tokenResponseJson))) + val auth = KeyCloakAuth("tokenurl", "clientId", "client secret", sendHttpRequest) val expected = KeyCloakAuthToken( "token", @@ -63,30 +61,30 @@ class KeyCloakAuthSpec extends CrunchTestLike { "profile email" ) - val token = Await.result(auth.getToken("user", "pass"), 30 seconds) + val token = Await.result(auth.getToken("user", "pass"), 30.seconds) token === expected } "When logging into Keycloak with an invalid username and password then I should handle the response" >> { - val auth = new KeyCloakAuth("tokenurl", "clientId", "client secret") { - def sendAndReceive: HttpRequest => Future[HttpResponse] = (_: HttpRequest) => { - Future(HttpResponse(400).withEntity(HttpEntity( - ContentTypes.`application/json`, - """| + val sendHttpRequest: HttpRequest => Future[HttpResponse] = (_: HttpRequest) => { + Future(HttpResponse(400).withEntity(HttpEntity( + ContentTypes.`application/json`, + """| |{ - | "error": "invalid_grant", - | "error_description": "Invalid user credentials" - |} + | "error": "invalid_grant", + | "error_description": "Invalid user credentials" + |} """.stripMargin - ))) - } + ))) } + val auth = KeyCloakAuth("tokenurl", "clientId", "client secret", sendHttpRequest) + val expected = KeyCloakAuthError("invalid_grant", "Invalid user credentials") - val errorResponse = Await.result(auth.getToken("user", "pass"), 30 seconds) + val errorResponse = Await.result(auth.getToken("user", "pass"), 30.seconds) errorResponse === expected } diff --git a/server/src/test/scala/services/exports/FlightExportSpec.scala b/server/src/test/scala/services/exports/FlightExportSpec.scala new file mode 100644 index 000000000..fc65dc5bb --- /dev/null +++ b/server/src/test/scala/services/exports/FlightExportSpec.scala @@ -0,0 +1,54 @@ +package services.exports + +import akka.actor.ActorSystem +import akka.stream.Materializer +import controllers.ArrivalGenerator +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import services.api.v1.FlightExport +import services.api.v1.FlightExport.{FlightJson, PortFlightsJson, TerminalFlightsJson} +import uk.gov.homeoffice.drt.ports.Terminals.{T1, Terminal} +import uk.gov.homeoffice.drt.ports.{FeedSource, LiveFeedSource, PortCode} +import uk.gov.homeoffice.drt.time.{SDate, SDateLike} + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, ExecutionContextExecutor, Future} + + +class FlightExportSpec extends AnyWordSpec with Matchers { + implicit val system: ActorSystem = ActorSystem("FlightExportSpec") + implicit val mat: Materializer = Materializer.matFromSystem + implicit val ec: ExecutionContextExecutor = system.dispatcher + implicit val sourceOrderPreference: List[FeedSource] = List(LiveFeedSource) + + val startMinute: SDateLike = SDate("2024-10-15T12:00") + val endMinute: SDateLike = SDate("2024-10-15T14:00") + + "FlightExport" should { + "return a PortFlightsJson with the correct structure and only the flight with passengers in the requested time range" in { + val source = (_: SDateLike, _: SDateLike, _: Terminal) => { + Future.successful(Seq( + ArrivalGenerator.arrival(iata = "BA0001", schDt = "2024-10-15T11:00", totalPax = Option(100), transPax = Option(10), feedSource = LiveFeedSource), + ArrivalGenerator.arrival(iata = "BA0002", schDt = "2024-10-15T12:00", totalPax = Option(100), transPax = Option(10), feedSource = LiveFeedSource), + ArrivalGenerator.arrival(iata = "BA0003", schDt = "2024-10-15T13:55", totalPax = Option(200), transPax = Option(10), feedSource = LiveFeedSource), + ArrivalGenerator.arrival(iata = "BA0004", schDt = "2024-10-15T15:00", totalPax = Option(200), transPax = Option(10), feedSource = LiveFeedSource), + )) + } + val export = FlightExport(source, Seq(T1), PortCode("LHR")) + val sched1 = SDate("2024-10-15T12:00") + val sched2 = SDate("2024-10-15T13:55") + Await.result(export(startMinute, endMinute), 1.second) shouldEqual + PortFlightsJson( + PortCode("LHR"), + List(TerminalFlightsJson( + T1, + List( + FlightJson("BA0002", "JFK", sched1.millisSinceEpoch, Some(sched1.addMinutes(5).millisSinceEpoch), Some(sched1.addMinutes(9).millisSinceEpoch), Some(90), ""), + FlightJson("BA0003", "JFK", sched2.millisSinceEpoch, Some(sched2.addMinutes(5).millisSinceEpoch), Some(sched2.addMinutes(14).millisSinceEpoch), Some(190), ""), + ) + ) + ) + ) + } + } +} diff --git a/server/src/test/scala/services/exports/QueueExportSpec.scala b/server/src/test/scala/services/exports/QueueExportSpec.scala index b755770f5..c60a83288 100644 --- a/server/src/test/scala/services/exports/QueueExportSpec.scala +++ b/server/src/test/scala/services/exports/QueueExportSpec.scala @@ -21,38 +21,49 @@ class QueueExportSpec extends AnyWordSpec with Matchers { implicit val mat: Materializer = Materializer.matFromSystem implicit val ec: ExecutionContextExecutor = system.dispatcher - val minute: SDateLike = SDate("2024-10-15T12:00") + val start: SDateLike = SDate("2024-10-15T12:00") + val end: SDateLike = SDate("2024-10-15T12:30") "QueueExport" should { - "return a PortQueuesJson with the correct structure" in { + "return a PortQueuesJson with the correct structure and only the values in the requested time range" in { val source = (_: SDateLike, _: SDateLike, _: Terminal, _: Int) => { Future.successful(Seq( - minute.millisSinceEpoch -> Seq( - CrunchMinute(T1, EeaDesk, minute.millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), - CrunchMinute(T1, NonEeaDesk, minute.millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), - CrunchMinute(T1, EGate, minute.millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), + start.addMinutes(-15).millisSinceEpoch -> Seq( + CrunchMinute(T1, EeaDesk, start.addMinutes(-15).millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, NonEeaDesk, start.addMinutes(-15).millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, EGate, start.addMinutes(-15).millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), ), - minute.addMinutes(15).millisSinceEpoch -> Seq( - CrunchMinute(T1, EeaDesk, minute.addMinutes(15).millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), - CrunchMinute(T1, NonEeaDesk, minute.addMinutes(15).millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), - CrunchMinute(T1, EGate, minute.addMinutes(15).millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), + start.millisSinceEpoch -> Seq( + CrunchMinute(T1, EeaDesk, start.millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, NonEeaDesk, start.millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, EGate, start.millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), + ), + start.addMinutes(15).millisSinceEpoch -> Seq( + CrunchMinute(T1, EeaDesk, start.addMinutes(15).millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, NonEeaDesk, start.addMinutes(15).millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, EGate, start.addMinutes(15).millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), + ), + start.addMinutes(30).millisSinceEpoch -> Seq( + CrunchMinute(T1, EeaDesk, start.addMinutes(30).millisSinceEpoch, 10d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, NonEeaDesk, start.addMinutes(30).millisSinceEpoch, 12d, 0d, 0, 0, None, None, None, None, None, None, None), + CrunchMinute(T1, EGate, start.addMinutes(30).millisSinceEpoch, 14d, 0d, 0, 0, None, None, None, None, None, None, None), ), )) } val export = QueueExport(source, Seq(T1), PortCode("LHR")) - Await.result(export(minute, minute, 15), 1.second) shouldEqual + Await.result(export(start, end, 15), 1.second) shouldEqual PortQueuesJson( PortCode("LHR"), Seq( TerminalQueuesJson( T1, Seq( - PeriodJson(minute, Seq( + PeriodJson(start, Seq( QueueJson(EeaDesk, 10, 0), QueueJson(NonEeaDesk, 12, 0), QueueJson(EGate, 14, 0), )), - PeriodJson(minute.addMinutes(15), Seq( + PeriodJson(start.addMinutes(15), Seq( QueueJson(EeaDesk, 10, 0), QueueJson(NonEeaDesk, 12, 0), QueueJson(EGate, 14, 0), From e36d3dd5e6fd4e234f1c2f809b62799cb872ad58 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Sat, 19 Oct 2024 11:16:26 +0100 Subject: [PATCH 09/13] DRTII-1608 Remove json files --- flights-export.json | 32 ------------------------- queues-export.json | 57 --------------------------------------------- 2 files changed, 89 deletions(-) delete mode 100644 flights-export.json delete mode 100644 queues-export.json diff --git a/flights-export.json b/flights-export.json deleted file mode 100644 index d53d43d43..000000000 --- a/flights-export.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "startTime": "2024-10-15T12:07", - "endTime": "2024-10-15T14:06", - "ports": [ - { - "name": "LHR", - "terminals": [ - { - "name": "T2", - "flights": [ - { - "code": "BA0001", - "origin": "JFK", - "scheduledTime": "2024-10-15T12:05", - "estimatedPcpArrivalStartTime": "2024-10-15T12:15", - "estimatedPcpArrivalEndTime": "2024-10-15T12:19", - "estimatedPassengerCount": 100 - }, - { - "code": "BA0002", - "origin": "CDG", - "scheduledTime": "2024-10-15T12:20", - "estimatedPcpArrivalStartTime": "2024-10-15T12:32", - "estimatedPcpArrivalEndTime": "2024-10-15T12:33", - "estimatedPassengerCount": 50 - } - ] - } - ] - } - ] -} diff --git a/queues-export.json b/queues-export.json deleted file mode 100644 index 4c24bde3d..000000000 --- a/queues-export.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "start-time": "2024-10-15T12:05", - "end-time": "2024-10-15T14:04", - "period-length-minutes": 15, - "ports": [ - { - "name": "LHR", - "terminals": [ - { - "name": "T1", - "periods": [ - { - "start-time": "2024-10-15T12:05", - "queues": [ - { - "name": "eea", - "pax-joining": 100, - "max-wait": 10 - }, - { - "name": "non-eea", - "pax-joining": 100, - "max-wait": 10 - }, - { - "name": "e-gates", - "pax-joining": 100, - "max-wait": 10 - } - ] - }, - { - "start-time": "2024-10-15T12:20", - "queues": [ - { - "name": "eea", - "pax-joining": 110, - "max-wait": 5 - }, - { - "name": "non-eea", - "pax-joining": 110, - "max-wait": 5 - }, - { - "name": "e-gates", - "pax-joining": 110, - "max-wait": 5 - } - ] - } - ] - } - ] - }, - ] -} From 01b598a1a321601aefea649277e90be306bde435 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Sat, 19 Oct 2024 11:29:09 +0100 Subject: [PATCH 10/13] DRTII-1608 remove keycloak tests (added to drt-lib) --- .../main/scala/controllers/Application.scala | 2 +- .../application/ImportsController.scala | 9 +- .../exports/StaffRequirementExports.scala | 6 +- .../src/test/scala/api/KeyCloakAuthSpec.scala | 92 ------------------- .../exports/StaffRequirementExportsSpec.scala | 2 +- 5 files changed, 10 insertions(+), 101 deletions(-) delete mode 100644 server/src/test/scala/api/KeyCloakAuthSpec.scala diff --git a/server/src/main/scala/controllers/Application.scala b/server/src/main/scala/controllers/Application.scala index 6aea01dae..8b8d589d5 100644 --- a/server/src/main/scala/controllers/Application.scala +++ b/server/src/main/scala/controllers/Application.scala @@ -252,7 +252,7 @@ class Application @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface)( val clientSecretOption = config.getOptional[String]("key-cloak.client_secret") val usernameOption = postStringValOrElse("username") val passwordOption = postStringValOrElse("password") -// import KeyCloakAuthTokenParserProtocol._ + import spray.json._ def tokenToHttpResponse(username: String)(token: KeyCloakAuthResponse): Result = token match { diff --git a/server/src/main/scala/controllers/application/ImportsController.scala b/server/src/main/scala/controllers/application/ImportsController.scala index af0a5529c..21888942e 100644 --- a/server/src/main/scala/controllers/application/ImportsController.scala +++ b/server/src/main/scala/controllers/application/ImportsController.scala @@ -26,11 +26,12 @@ import scala.util.Try case class ApiResponseBody(message: String) + object ApiResponseBody { implicit val w: OWrites[ApiResponseBody] = writes[ApiResponseBody] } -class ImportsController@Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) { +class ImportsController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInterface) extends AuthController(cc, ctrl) { def feedImportRedListCounts: Action[AnyContent] = authByRole(NeboUpload) { Action.async { request => @@ -46,9 +47,9 @@ class ImportsController@Inject()(cc: ControllerComponents, ctrl: DrtSystemInterf .ask(RedListCounts(updatedRedListCounts)) Accepted(toJson(ApiResponseBody(s"${redListCounts.passengers} red list records imported"))) }.recover { - case e => log.warning(s"Error while updating redListPassenger", e) - BadRequest("Failed to update the red List Passenger") - } + case e => log.warning(s"Error while updating redListPassenger", e) + BadRequest("Failed to update the red List Passenger") + } }.getOrElse(Future.successful(BadRequest("Failed to parse json"))) case None => Future.successful(BadRequest("No content")) } diff --git a/server/src/main/scala/services/exports/StaffRequirementExports.scala b/server/src/main/scala/services/exports/StaffRequirementExports.scala index 870530068..a9d6f865e 100644 --- a/server/src/main/scala/services/exports/StaffRequirementExports.scala +++ b/server/src/main/scala/services/exports/StaffRequirementExports.scala @@ -53,8 +53,8 @@ object StaffRequirementExports { val numberOfSlots = 1440 / minutesInSlot val dateFormatted = f"${date.day}%02d/${date.month}%02d" staffProvider(date).map { staffMinutes => - val staffMinutesBySlot = groupByXMinutes(staffMinutes, minutesInSlot) - val crunchMinutesBySlot = groupByXMinutes(crunchMinutes, minutesInSlot) + val staffMinutesBySlot = groupByMinutes(staffMinutes, minutesInSlot) + val crunchMinutesBySlot = groupByMinutes(crunchMinutes, minutesInSlot) val availableAndRequired: Seq[(String, String, String)] = (0 until numberOfSlots).map { slotNumber => val slotCrunch = crunchMinutesBySlot.getOrElse(slotNumber, Seq()) @@ -80,7 +80,7 @@ object StaffRequirementExports { if (reqs.nonEmpty) reqs.max else 0 } - def groupByXMinutes[A, B](minutes: Seq[MinuteLike[A, B]], minutesInGroup: Int): Map[Int, Seq[A]] = + def groupByMinutes[A, B](minutes: Seq[MinuteLike[A, B]], minutesInGroup: Int): Map[Int, Seq[A]] = minutes .groupBy { sm => val localSDate = SDate(sm.minute, europeLondonTimeZone) diff --git a/server/src/test/scala/api/KeyCloakAuthSpec.scala b/server/src/test/scala/api/KeyCloakAuthSpec.scala deleted file mode 100644 index 4e61f4415..000000000 --- a/server/src/test/scala/api/KeyCloakAuthSpec.scala +++ /dev/null @@ -1,92 +0,0 @@ -package api - -import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpRequest, HttpResponse} -import services.crunch.CrunchTestLike -import uk.gov.homeoffice.drt.keycloak.{KeyCloakAuth, KeyCloakAuthError, KeyCloakAuthToken, KeyCloakAuthTokenParserProtocol} - -import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} - -class KeyCloakAuthSpec extends CrunchTestLike with KeyCloakAuthTokenParserProtocol { - - val keyCloakUrl = "https://keycloak" - - val tokenResponseJson: String = - s"""{ - | "access_token": "token", - | "expires_in": 86400, - | "refresh_expires_in": 86400, - | "refresh_token": "refresh token", - | "token_type": "bearer", - | "not-before-policy": 0, - | "session_state": "session", - | "scope": "profile email" - |}""".stripMargin - - - "When parsing keycloak JSON token I should get back a case class representation of the token" >> { - - import spray.json._ - - val expected = KeyCloakAuthToken( - "token", - 86400, - 86400, - "refresh token", - "bearer", - 0, - "session", - "profile email" - ) - - val result: KeyCloakAuthToken = tokenResponseJson.parseJson.convertTo[KeyCloakAuthToken] - - result === expected - } - - "When logging into Keycloak with a correct username and password then I should get a token back" >> { - - val sendHttpRequest: HttpRequest => Future[HttpResponse] = - _ => Future.successful(HttpResponse().withEntity(HttpEntity(ContentTypes.`application/json`, tokenResponseJson))) - val auth = KeyCloakAuth("tokenurl", "clientId", "client secret", sendHttpRequest) - - val expected = KeyCloakAuthToken( - "token", - 86400, - 86400, - "refresh token", - "bearer", - 0, - "session", - "profile email" - ) - - val token = Await.result(auth.getToken("user", "pass"), 30.seconds) - - token === expected - } - - "When logging into Keycloak with an invalid username and password then I should handle the response" >> { - - val sendHttpRequest: HttpRequest => Future[HttpResponse] = (_: HttpRequest) => { - Future(HttpResponse(400).withEntity(HttpEntity( - ContentTypes.`application/json`, - """| - |{ - | "error": "invalid_grant", - | "error_description": "Invalid user credentials" - |} - """.stripMargin - ))) - } - - val auth = KeyCloakAuth("tokenurl", "clientId", "client secret", sendHttpRequest) - - val expected = KeyCloakAuthError("invalid_grant", "Invalid user credentials") - - val errorResponse = Await.result(auth.getToken("user", "pass"), 30.seconds) - - errorResponse === expected - } - -} diff --git a/server/src/test/scala/services/exports/StaffRequirementExportsSpec.scala b/server/src/test/scala/services/exports/StaffRequirementExportsSpec.scala index 29c4e93fb..34c6efcfa 100644 --- a/server/src/test/scala/services/exports/StaffRequirementExportsSpec.scala +++ b/server/src/test/scala/services/exports/StaffRequirementExportsSpec.scala @@ -21,7 +21,7 @@ class StaffRequirementExportsSpec extends Specification { def fifteenMins(start: SDateLike): Seq[Long] = (start.millisSinceEpoch to start.addMinutes(14).millisSinceEpoch by oneMinuteMillis).toList - val grouped = StaffRequirementExports.groupByXMinutes(minutes, 15).view.mapValues(_.map(_.minute)).toMap + val grouped = StaffRequirementExports.groupByMinutes(minutes, 15).view.mapValues(_.map(_.minute)).toMap val expected = Map( 0 -> fifteenMins(SDate("2023-09-26T00:00", europeLondonTimeZone)), 1 -> fifteenMins(SDate("2023-09-26T00:15", europeLondonTimeZone)), From 6c3019d95365278b6496d3a8933ba7d95727e643 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Tue, 29 Oct 2024 14:32:36 +0000 Subject: [PATCH 11/13] DRTII-1608 Add role auth protection to api routes --- client/package-lock.json | 2 +- project/Settings.scala | 2 +- .../application/api/v1/FlightsApiController.scala | 5 +++-- .../controllers/application/api/v1/QueuesApiController.scala | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 30f6f5467..f3b1760bc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,5 +1,5 @@ { - "name": "test", + "name": "main", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/project/Settings.scala b/project/Settings.scala index e493e0e4b..e4d387fba 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -25,7 +25,7 @@ object Settings { /** Declare global dependency versions here to avoid mismatches in multi part dependencies */ //noinspection ScalaStyle object versions { - val drtLib = "v903" + val drtLib = "v910" val scala = "2.13.12" val scalaDom = "2.8.0" diff --git a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala index 95c9ceac7..4f947d394 100644 --- a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala @@ -11,6 +11,7 @@ import services.api.v1.FlightExport import services.api.v1.serialisation.FlightApiJsonProtocol import spray.json.enrichAny import uk.gov.homeoffice.drt.arrivals.{Arrival, FlightsWithSplits} +import uk.gov.homeoffice.drt.auth.Roles.ApiFlightAccess import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.ports.FeedSource import uk.gov.homeoffice.drt.ports.Terminals.Terminal @@ -47,7 +48,7 @@ class FlightsApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemIn FlightExport(flightTotalsForGranularity, ctrl.airportConfig.terminals, ctrl.airportConfig.portCode) def flights(): Action[AnyContent] = - auth( + authByRole(ApiFlightAccess) { Action.async { request => val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) @@ -59,7 +60,7 @@ class FlightsApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemIn flightExport(start, end).map(r => Ok(r.toJson.compactPrint)) } - ) + } private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = maybeString match { diff --git a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala index ddba6ee63..f2aed01c7 100644 --- a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala @@ -10,6 +10,7 @@ import play.api.mvc._ import services.api.v1.QueueExport import services.api.v1.serialisation.QueueApiJsonProtocol import spray.json.enrichAny +import uk.gov.homeoffice.drt.auth.Roles.ApiQueueAccess import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.ports.Queues import uk.gov.homeoffice.drt.ports.Terminals.Terminal @@ -34,7 +35,7 @@ class QueuesApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt QueueExport(queueTotalsForGranularity, ctrl.airportConfig.terminals, ctrl.airportConfig.portCode) def queues(): Action[AnyContent] = - auth( + authByRole(ApiQueueAccess) { Action.async { request => val start = parseOptionalEndDate(request.getQueryString("start"), SDate.now()) @@ -46,7 +47,7 @@ class QueuesApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt queueExport(start, end, periodMinutes).map(r => Ok(r.toJson.compactPrint)) } - ) + } private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = maybeString match { From af6e0af2c5933974807389b0a624b9d5a9e2d835 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Tue, 29 Oct 2024 14:47:19 +0000 Subject: [PATCH 12/13] DRTII-1608 Tidying --- client/package-lock.json | 745 +++++++++++++++++- project/Settings.scala | 2 +- .../application/SummariesController.scala | 8 +- .../scala/services/api/v1/QueueExport.scala | 4 +- 4 files changed, 711 insertions(+), 48 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index f3b1760bc..fc6da1700 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,5 +1,5 @@ { - "name": "main", + "name": "test", "lockfileVersion": 2, "requires": true, "packages": { @@ -1849,9 +1849,9 @@ }, "node_modules/@drt/drt-react": { "name": "@drt/drt-react-components", - "version": "1.0.0", - "resolved": "git+ssh://git@github.com/UKHomeOffice/drt-react.git#f0f7f2ae2eebd7dddfeb92be9d7dac06204647d8", - "integrity": "sha512-X0o9d7Rutr66Wm9tHuUi4mmnpTMye8QRlv93Lwcn8GJsDlUYN6CNw0DY7g96wTf3KaaEia06GQAX3pPIo63gxQ==", + "version": "1.5.1", + "resolved": "git+ssh://git@github.com/UKHomeOffice/drt-react.git#a31cd014e8913b756f5d901186d4e271e9994b82", + "integrity": "sha512-CeRBLMN8qdsoLvv3iEHHufqUtnjJp5YRtwN+Zd2WZcwvx4WAklSY1KI5F9bomvxkTVZjUEeo3OmEx3Ko+04jAg==", "license": "MIT", "dependencies": { "-": "^0.0.1", @@ -1859,15 +1859,21 @@ "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.18", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "5.11.16", + "@mui/icons-material": "5.16.5", "@mui/lab": "5.0.0-alpha.119", "@mui/material": "5.16.5", + "@mui/x-date-pickers": "^6.20.2", "@storybook/addon-a11y": "^8.1.10", "@storybook/icons": "^1.2.9", "@svgr/webpack": "^8.1.0", "css-loader": "^7.1.2", "css-mediaquery": "^0.1.2", + "dayjs": "^1.11.13", + "esbuild": "^0.24.0", "install-peers": "^1.0.4", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "momentjs": "^2.0.0", "postcss": "^8.4.38", "sass-loader": "^14.2.1", "style-loader": "^4.0.0" @@ -1932,29 +1938,349 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, - "node_modules/@drt/drt-react/node_modules/@mui/icons-material": { - "version": "5.11.16", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.16.tgz", - "integrity": "sha512-oKkx9z9Kwg40NtcIajF9uOXhxiyTZrrm9nmIJ4UjkU2IdHpd4QVLbCc/5hZN/y0C6qzi2Zlxyr9TGddQx2vx2A==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, + "node_modules/@drt/drt-react/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui" - }, - "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@drt/drt-react/node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@drt/drt-react/node_modules/@mui/lab": { @@ -2072,6 +2398,55 @@ } } }, + "node_modules/@drt/drt-react/node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/@drt/drt-react/node_modules/moment-timezone": { + "version": "0.5.46", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", + "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/@drt/drt-react/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -2517,6 +2892,21 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -3000,6 +3390,71 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.20.2.tgz", + "integrity": "sha512-x1jLg8R+WhvkmUETRfX2wC+xJreMii78EXKLl6r3G+ggcAZlPyt0myID1Amf6hvJb9CtR7CgUo8BwR+1Vx9Ggw==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5547,6 +6002,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -9456,6 +9916,12 @@ "node": "*" } }, + "node_modules/momentjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/momentjs/-/momentjs-2.0.0.tgz", + "integrity": "sha512-GYMUxLyCwVhECkJR1/LMHEyb9gWYSPRnXi+elGN0m5bet7ngQOxU4QLWUI/eBzgN4N/T194n6yP7lQiE+Udw9A==", + "deprecated": "WARNING: The correct package name for Moment.js is 'moment', not 'momentjs'." + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -15034,8 +15500,8 @@ "dev": true }, "@drt/drt-react": { - "version": "git+ssh://git@github.com/UKHomeOffice/drt-react.git#f0f7f2ae2eebd7dddfeb92be9d7dac06204647d8", - "integrity": "sha512-X0o9d7Rutr66Wm9tHuUi4mmnpTMye8QRlv93Lwcn8GJsDlUYN6CNw0DY7g96wTf3KaaEia06GQAX3pPIo63gxQ==", + "version": "git+ssh://git@github.com/UKHomeOffice/drt-react.git#a31cd014e8913b756f5d901186d4e271e9994b82", + "integrity": "sha512-CeRBLMN8qdsoLvv3iEHHufqUtnjJp5YRtwN+Zd2WZcwvx4WAklSY1KI5F9bomvxkTVZjUEeo3OmEx3Ko+04jAg==", "from": "@drt/drt-react@https://github.com/UKHomeOffice/drt-react/tree/1.4.1", "requires": { "-": "^0.0.1", @@ -15043,15 +15509,21 @@ "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.18", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "5.11.16", + "@mui/icons-material": "5.16.5", "@mui/lab": "5.0.0-alpha.119", "@mui/material": "5.16.5", + "@mui/x-date-pickers": "^6.20.2", "@storybook/addon-a11y": "^8.1.10", "@storybook/icons": "^1.2.9", "@svgr/webpack": "^8.1.0", "css-loader": "^7.1.2", "css-mediaquery": "^0.1.2", + "dayjs": "^1.11.13", + "esbuild": "^0.24.0", "install-peers": "^1.0.4", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "momentjs": "^2.0.0", "postcss": "^8.4.38", "sass-loader": "^14.2.1", "style-loader": "^4.0.0" @@ -15090,13 +15562,143 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, - "@mui/icons-material": { - "version": "5.11.16", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.16.tgz", - "integrity": "sha512-oKkx9z9Kwg40NtcIajF9uOXhxiyTZrrm9nmIJ4UjkU2IdHpd4QVLbCc/5hZN/y0C6qzi2Zlxyr9TGddQx2vx2A==", - "requires": { - "@babel/runtime": "^7.21.0" - } + "@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "optional": true }, "@mui/lab": { "version": "5.0.0-alpha.119", @@ -15150,6 +15752,45 @@ "semver": "^7.5.4" } }, + "esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "requires": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "moment-timezone": { + "version": "0.5.46", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", + "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", + "requires": { + "moment": "^2.29.4" + } + }, "semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -15404,6 +16045,12 @@ "optional": true, "peer": true }, + "@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "optional": true + }, "@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -15652,6 +16299,20 @@ "react-is": "^18.3.1" } }, + "@mui/x-date-pickers": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.20.2.tgz", + "integrity": "sha512-x1jLg8R+WhvkmUETRfX2wC+xJreMii78EXKLl6r3G+ggcAZlPyt0myID1Amf6hvJb9CtR7CgUo8BwR+1Vx9Ggw==", + "requires": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -17585,6 +18246,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -20431,6 +21097,11 @@ "moment": ">= 2.9.0" } }, + "momentjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/momentjs/-/momentjs-2.0.0.tgz", + "integrity": "sha512-GYMUxLyCwVhECkJR1/LMHEyb9gWYSPRnXi+elGN0m5bet7ngQOxU4QLWUI/eBzgN4N/T194n6yP7lQiE+Udw9A==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/project/Settings.scala b/project/Settings.scala index e4d387fba..dd39df693 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -25,7 +25,7 @@ object Settings { /** Declare global dependency versions here to avoid mismatches in multi part dependencies */ //noinspection ScalaStyle object versions { - val drtLib = "v910" + val drtLib = "v914" val scala = "2.13.12" val scalaDom = "2.8.0" diff --git a/server/src/main/scala/controllers/application/SummariesController.scala b/server/src/main/scala/controllers/application/SummariesController.scala index 208ca8dd4..6a1382a20 100644 --- a/server/src/main/scala/controllers/application/SummariesController.scala +++ b/server/src/main/scala/controllers/application/SummariesController.scala @@ -15,7 +15,7 @@ import uk.gov.homeoffice.drt.ports.Queues.Queue import uk.gov.homeoffice.drt.ports.Terminals.Terminal import uk.gov.homeoffice.drt.ports.{PortRegion, Queues} import uk.gov.homeoffice.drt.time.TimeZoneHelper.europeLondonTimeZone -import uk.gov.homeoffice.drt.time.{DateRange, LocalDate, SDate, SDateLike} +import uk.gov.homeoffice.drt.time.{DateRange, LocalDate, SDate} import scala.concurrent.Future import scala.util.{Failure, Success, Try} @@ -36,12 +36,6 @@ class SummariesController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt } } - private def parseOptionalEndDate(maybeString: Option[String], default: SDateLike): SDateLike = - maybeString match { - case None => default - case Some(dateStr) => SDate(dateStr) - } - def exportPassengersByTerminalForDateRangeApi(startLocalDateString: String, endLocalDateString: String, terminalName: String): Action[AnyContent] = diff --git a/server/src/main/scala/services/api/v1/QueueExport.scala b/server/src/main/scala/services/api/v1/QueueExport.scala index 15e6999eb..02e44c0c6 100644 --- a/server/src/main/scala/services/api/v1/QueueExport.scala +++ b/server/src/main/scala/services/api/v1/QueueExport.scala @@ -37,9 +37,7 @@ object QueueExport { .map { queueTotals: Iterable[(MillisSinceEpoch, Seq[CrunchMinute])] => queueTotals .filter { - case (slotTime, _) => - println(s"${start.toISOString} <= ${SDate(slotTime).toISOString} < ${end.toISOString}") - start.millisSinceEpoch <= slotTime && slotTime < end.millisSinceEpoch + case (slotTime, _) => start.millisSinceEpoch <= slotTime && slotTime < end.millisSinceEpoch } .map { case (slotTime, queues) => PeriodJson(SDate(slotTime), queues.map(QueueJson.apply)) From 0a0e84266bf1de34968faaa44aaf37a167c890da Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Tue, 29 Oct 2024 15:23:14 +0000 Subject: [PATCH 13/13] DRTII-1608 move test flight an hour later to be crunched for today --- e2e/cypress/e2e/multi-day-export.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/cypress/e2e/multi-day-export.cy.ts b/e2e/cypress/e2e/multi-day-export.cy.ts index 8718c1f5d..e534bd28b 100644 --- a/e2e/cypress/e2e/multi-day-export.cy.ts +++ b/e2e/cypress/e2e/multi-day-export.cy.ts @@ -48,7 +48,7 @@ describe('Multi day export', () => { cy .addFlight({ "SchDT": todayAtUtcString(0, 55), - "ActChoxDT": todayAtUtcString(3, 2), + "ActChoxDT": todayAtUtcString(4, 2), "ActPax": 51 }) .asABorderForceOfficer()