diff --git a/app/dao/CosmosQuery.scala b/app/dao/CosmosQuery.scala index b42bc28..c7c8fae 100644 --- a/app/dao/CosmosQuery.scala +++ b/app/dao/CosmosQuery.scala @@ -34,6 +34,16 @@ object CosmosQuery { |AND ARRAY_LENGTH(c.imageLinks) >= 1 |OFFSET 0 LIMIT 10""".stripMargin) + def getInStockInventoryAddedInLastWeek(sevenDaysAgo: String)(collectionName: String): SqlQuerySpec = + new SqlQuerySpec( + s"""SELECT * FROM $collectionName c + |WHERE c.status != "Sold" + |AND c.status != "Pending Sale" + |AND ARRAY_LENGTH(c.imageLinks) >= 1 + |AND c.creationTimeStamp >= @sevenDaysAgo""".stripMargin, + List(new SqlParameter("@sevenDaysAgo", sevenDaysAgo)): _* + ) + def getNotificationWithinWindow(date: String)(collectionName: String): SqlQuerySpec = { new SqlQuerySpec( s"""SELECT * FROM $collectionName c diff --git a/app/models/MayberryMiniTrucks.scala b/app/models/MayberryMiniTrucks.scala index a2d26cd..8fc8367 100644 --- a/app/models/MayberryMiniTrucks.scala +++ b/app/models/MayberryMiniTrucks.scala @@ -111,4 +111,18 @@ case class ContactRequest( description: String, vin: String = "", isFailedFilter: Boolean = false - ) \ No newline at end of file + ) + + +case class InventoryDetailsForTemplate( + photoUrl: String, + year: String, + make: String, + model: String, + price: String, + mileage: String, + engine: String, + transmission: String, + color: String, + itemUrl: String + ) \ No newline at end of file diff --git a/app/modules/Injection.scala b/app/modules/Injection.scala index adf4481..8466a75 100644 --- a/app/modules/Injection.scala +++ b/app/modules/Injection.scala @@ -2,6 +2,7 @@ package modules import com.google.inject.AbstractModule import dao.{CosmosDb, CosmosDbBuilder} +import services.InventoryNewsLetterService class Injection extends AbstractModule { @@ -9,5 +10,6 @@ class Injection extends AbstractModule { override def configure(): Unit = { bind(classOf[CosmosDbBuilder]).asEagerSingleton() bind(classOf[CosmosDb]).asEagerSingleton() + bind(classOf[InventoryNewsLetterService]).asEagerSingleton() } } \ No newline at end of file diff --git a/app/services/InventoryNewsLetterService.scala b/app/services/InventoryNewsLetterService.scala new file mode 100644 index 0000000..c01f20a --- /dev/null +++ b/app/services/InventoryNewsLetterService.scala @@ -0,0 +1,80 @@ +package services + +import akka.actor.{ActorSystem, Cancellable} +import dao.{CosmosDb, CosmosQuery} +import dao.CosmosQuery.{getAllResults, getInStockInventoryAddedInLastWeek} +import models.{Inventory, Subscribers} +import shared.AppFunctions._ + +import java.time.format.DateTimeFormatter +import java.time.temporal.{ChronoUnit, TemporalAdjusters} +import java.time.{DayOfWeek, Duration, Instant, ZoneId, ZonedDateTime} +import javax.inject.{Inject, Singleton} +import scala.concurrent.ExecutionContext +import scala.concurrent.duration.{DurationInt, FiniteDuration, MILLISECONDS} + +@Singleton +class InventoryNewsLetterService @Inject()(actorSystem: ActorSystem, + cosmosDb: CosmosDb, + emailService: EmailService) + (implicit ec: ExecutionContext) { + private val inventoryCollection: String = CosmosQuery.inventoryCollection + private val subscriberCollection: String = CosmosQuery.subscriberCollection + + // Define the task to run + def task(): Unit = { + println("Running scheduled task at " + ZonedDateTime.now(ZoneId.of("America/New_York"))) + val sevenDaysAgo = Instant.now().minus(7, ChronoUnit.DAYS) + val formattedDate = DateTimeFormatter.ISO_INSTANT.format(sevenDaysAgo) + + for { + inventoryList <- cosmosDb.runQuery[Inventory](getInStockInventoryAddedInLastWeek(formattedDate), inventoryCollection) + inventoryMap = inventoryList.zipWithIndex.flatMap { + case (vehicle, index) => Map( + s"photoUrl$index" -> vehicle.imageLinks.head, + s"year$index" -> vehicle.year, + s"make$index" -> vehicle.make, + s"model$index" -> vehicle.model, + s"price$index" -> formatPrice(vehicle.price), + s"mileage$index" -> formatNumberWithCommas(vehicle.mileage.toString), + s"engine$index" -> vehicle.engine, + s"transmission$index" -> vehicle.transmission, + s"color$index" -> vehicle.exteriorColor, + s"itemURL$index" -> s"http://localhost:3000/inventory/${vehicle.vin}" + ) + }.toMap + subscriberList <- cosmosDb.runQuery[Subscribers](getAllResults(), subscriberCollection) + _ = subscriberList.map(subscriber => + if(inventoryList.length >= 6) { + emailService.sendEmail(subscriber.email, "d-965b9c678e364190a1d8aef756faa080", inventoryMap) + } else if (inventoryList.length >= 3) { + emailService.sendEmail(subscriber.email, "d-287fccd69f8e479c88b1234cb078b5f1", inventoryMap) + } + ) + } yield "" + } + + // Calculate the initial delay until the next Wednesday at 9 AM EST + def calculateInitialDelay(): FiniteDuration = { + val now = ZonedDateTime.now(ZoneId.of("America/New_York")) + val nextRun = now.`with`(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY)) + .withHour(9) + .withMinute(0) + .withSecond(0) + .withNano(0) + + val initialDelay = Duration.between(now, nextRun) + FiniteDuration(initialDelay.toMillis, MILLISECONDS) + } + + // Schedule the task to run weekly on Wednesdays at 9 AM EST + private val cancellable: Cancellable = actorSystem.scheduler.scheduleAtFixedRate( + calculateInitialDelay(), //Change me to 10.millis to run quicker at startup + 7.days + )(new Runnable { + override def run(): Unit = task() + }) + + // Optionally add a stop hook for cleanup on application shutdown + def cancel(): Unit = cancellable.cancel() +} \ No newline at end of file diff --git a/app/shared/AppFunctions.scala b/app/shared/AppFunctions.scala index 094cc53..0405c1a 100644 --- a/app/shared/AppFunctions.scala +++ b/app/shared/AppFunctions.scala @@ -10,6 +10,8 @@ import play.api.mvc.{AnyContent, Request, Result} import play.api.mvc.Results.{NotFound, _} import java.sql.Timestamp +import java.text.NumberFormat +import java.util.Locale object AppFunctions { val objectMapper: JsonMapper with ClassTagExtensions = JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions @@ -49,4 +51,19 @@ object AppFunctions { def toSha256(message: String): String = String.format("%064x", new java.math.BigInteger(1, java.security.MessageDigest.getInstance("SHA-256").digest(message.getBytes("UTF-8")))) + + def formatPrice(price: BigDecimal): String = { + val formatter = NumberFormat.getCurrencyInstance(Locale.US) + formatter.format(price) + } + + def formatNumberWithCommas(numberString: String): String = { + try { + val number = numberString.toDouble + val formatter = NumberFormat.getInstance + formatter.format(number) + } catch { + case _: NumberFormatException => "Invalid number" + } + } }