diff --git a/commercial/app/AppLoader.scala b/commercial/app/AppLoader.scala index 171332b9ee31..8698c76a4008 100644 --- a/commercial/app/AppLoader.scala +++ b/commercial/app/AppLoader.scala @@ -5,7 +5,6 @@ import commercial.CommercialLifecycle import commercial.controllers.{CommercialControllers, HealthCheck} import commercial.model.capi.CapiAgent import commercial.model.feeds.{FeedsFetcher, FeedsParser} -import commercial.model.merchandise.books.{BestsellersAgent, BookFinder, MagentoService} import commercial.model.merchandise.events.{LiveEventAgent, MasterclassAgent} import commercial.model.merchandise.jobs.{Industries, JobsAgent} import commercial.model.merchandise.travel.TravelOffersAgent @@ -38,12 +37,9 @@ trait CommercialServices { def pekkoActorSystem: PekkoActorSystem implicit val executionContext: ExecutionContext - lazy val magentoService = wire[MagentoService] lazy val capiHttpClient: HttpClient = wire[CapiHttpClient] lazy val contentApiClient = wire[ContentApiClient] - lazy val bookFinder = wire[BookFinder] - lazy val bestsellersAgent = wire[BestsellersAgent] lazy val liveEventAgent = wire[LiveEventAgent] lazy val masterclassAgent = wire[MasterclassAgent] lazy val travelOffersAgent = wire[TravelOffersAgent] diff --git a/commercial/app/controllers/BookOffersController.scala b/commercial/app/controllers/BookOffersController.scala deleted file mode 100644 index f0d67f425b6d..000000000000 --- a/commercial/app/controllers/BookOffersController.scala +++ /dev/null @@ -1,55 +0,0 @@ -package commercial.controllers - -import commercial.model.Segment -import commercial.model.merchandise.Book -import commercial.model.merchandise.books.{BestsellersAgent, BookFinder} -import common.{ImplicitControllerExecutionContext, JsonComponent, JsonNotFound, GuLogging} -import model.Cached -import play.api.libs.json.{JsNull, JsValue, Json} -import play.api.mvc._ - -import scala.concurrent.duration._ - -class BookOffersController( - bookFinder: BookFinder, - bestsellersAgent: BestsellersAgent, - val controllerComponents: ControllerComponents, -) extends BaseController - with ImplicitControllerExecutionContext - with GuLogging - with implicits.Requests { - - private def booksSample(isbns: Seq[String], segment: Segment): Seq[Book] = - (bestsellersAgent.getSpecificBooks(isbns) ++ bestsellersAgent.bestsellersTargetedAt(segment)) - .distinctBy(_.isbn) - .take(4) - - private def isValidIsbn(isbn: String): Boolean = (isbn forall (_.isDigit)) && (isbn.length == 10 || isbn.length == 13) - - def getBook: Action[AnyContent] = - Action { implicit request => - lazy val failedLookupResult: Result = Cached(30.seconds)(JsonNotFound())(request) - lazy val badRequestResponse: Result = Cached(1.day)(JsonComponent.fromWritable(JsNull))(request) - - specificId match { - case Some(isbn) if isValidIsbn(isbn) => - bookFinder.findByIsbn(isbn) map { book: Book => - Cached(1.hour)(JsonComponent.fromWritable(book)) - } getOrElse failedLookupResult - case Some(invalidIsbn) => - log.error(s"Book lookup called with invalid ISBN '$invalidIsbn'. Returning empty response.") - badRequestResponse - case None => - log.error(s"Book lookup called with no ISBN. Returning empty response.") - badRequestResponse - - } - } - - def getBooks: Action[AnyContent] = - Action { implicit request => - Cached(60.seconds) { - JsonComponent.fromWritable(booksSample(specificIds, segment)) - } - } -} diff --git a/commercial/app/controllers/CommercialControllers.scala b/commercial/app/controllers/CommercialControllers.scala index ea2b8886c3ed..76bc338f3a4d 100644 --- a/commercial/app/controllers/CommercialControllers.scala +++ b/commercial/app/controllers/CommercialControllers.scala @@ -2,7 +2,6 @@ package commercial.controllers import com.softwaremill.macwire._ import commercial.model.capi.CapiAgent -import commercial.model.merchandise.books.{BestsellersAgent, BookFinder} import commercial.model.merchandise.events.{LiveEventAgent, MasterclassAgent} import commercial.model.merchandise.jobs.JobsAgent import commercial.model.merchandise.travel.TravelOffersAgent @@ -12,16 +11,13 @@ import play.api.mvc.ControllerComponents trait CommercialControllers { def contentApiClient: ContentApiClient - def bestsellersAgent: BestsellersAgent def liveEventAgent: LiveEventAgent - def bookFinder: BookFinder def capiAgent: CapiAgent def masterclassAgent: MasterclassAgent def travelOffersAgent: TravelOffersAgent def jobsAgent: JobsAgent def controllerComponents: ControllerComponents implicit def appContext: ApplicationContext - lazy val bookOffersController = wire[BookOffersController] lazy val contentApiOffersController = wire[ContentApiOffersController] lazy val creativeTestPage = wire[CreativeTestPage] lazy val hostedContentController = wire[HostedContentController] diff --git a/commercial/app/controllers/Multi.scala b/commercial/app/controllers/Multi.scala index 603d8a029da7..d618c6157baf 100644 --- a/commercial/app/controllers/Multi.scala +++ b/commercial/app/controllers/Multi.scala @@ -1,7 +1,6 @@ package commercial.controllers import commercial.model.Segment -import commercial.model.merchandise.books.BestsellersAgent import commercial.model.merchandise.events.MasterclassAgent import commercial.model.merchandise.jobs.JobsAgent import commercial.model.merchandise.travel.TravelOffersAgent @@ -12,7 +11,6 @@ import play.api.libs.json.{JsArray, Json} import play.api.mvc._ class Multi( - bestsellersAgent: BestsellersAgent, masterclassAgent: MasterclassAgent, travelOffersAgent: TravelOffersAgent, jobsAgent: JobsAgent, @@ -28,16 +26,6 @@ class Multi( val components: Seq[(String, Option[String])] = offerTypes zip offerIds components flatMap { - - case ("Book", Some(bookId)) => - bestsellersAgent.getSpecificBooks(Seq(bookId)) match { - case Nil => bestsellersAgent.bestsellersTargetedAt(segment) - case books => books - } - - case ("Book", None) => - bestsellersAgent.bestsellersTargetedAt(segment) - case ("Job", Some(jobId)) => jobsAgent.specificJobs(Seq(jobId)) diff --git a/commercial/app/model/feeds/FeedFetcher.scala b/commercial/app/model/feeds/FeedFetcher.scala index 57b1c81d4b0f..9bd6d8317f13 100644 --- a/commercial/app/model/feeds/FeedFetcher.scala +++ b/commercial/app/model/feeds/FeedFetcher.scala @@ -99,12 +99,6 @@ class FeedsFetcher(wsClient: WSClient) { } } - private val bestsellers: Option[FeedFetcher] = { - Configuration.commercial.magento.domain map { domain => - new SingleFeedFetcher(wsClient)(BestsellersFeedMetaData(domain)) - } - } - private val masterclasses: Option[FeedFetcher] = Configuration.commercial.masterclassesToken map (token => new EventbriteMultiPageFeedFetcher(wsClient)(EventsFeedMetaData("masterclasses", token)), @@ -120,7 +114,7 @@ class FeedsFetcher(wsClient: WSClient) { new SingleFeedFetcher(wsClient)(TravelOffersFeedMetaData(url)) } - val all: Seq[FeedFetcher] = Seq(bestsellers, masterclasses, travelOffers, jobs, liveEvents).flatten + val all: Seq[FeedFetcher] = Seq(masterclasses, travelOffers, jobs, liveEvents).flatten } diff --git a/commercial/app/model/feeds/FeedMetaData.scala b/commercial/app/model/feeds/FeedMetaData.scala index 692862617f6a..f61ad91d0a89 100644 --- a/commercial/app/model/feeds/FeedMetaData.scala +++ b/commercial/app/model/feeds/FeedMetaData.scala @@ -21,14 +21,6 @@ case class JobsFeedMetaData(override val url: String) extends FeedMetaData { override val responseEncoding = utf8 } -case class BestsellersFeedMetaData(domain: String) extends FeedMetaData { - - val name = "bestsellers" - val url = s"https://$domain/bertrams/feed/independentsTop20" - - override val responseEncoding = utf8 -} - case class EventsFeedMetaData( feedName: String, accessToken: String, diff --git a/commercial/app/model/feeds/FeedParser.scala b/commercial/app/model/feeds/FeedParser.scala index e63f0bd69d95..5e0eaf3416aa 100644 --- a/commercial/app/model/feeds/FeedParser.scala +++ b/commercial/app/model/feeds/FeedParser.scala @@ -1,11 +1,10 @@ package commercial.model.feeds -import commercial.model.merchandise.books.BestsellersAgent import commercial.model.merchandise.events.{LiveEventAgent, MasterclassAgent} import commercial.model.merchandise.jobs.JobsAgent import commercial.model.merchandise.travel.TravelOffersAgent import conf.Configuration -import commercial.model.merchandise.{Book, Job, LiveEvent, Masterclass, TravelOffer} +import commercial.model.merchandise.{Job, LiveEvent, Masterclass, TravelOffer} import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration @@ -17,7 +16,6 @@ sealed trait FeedParser[+T] { } class FeedsParser( - bestsellersAgent: BestsellersAgent, liveEventAgent: LiveEventAgent, masterclassAgent: MasterclassAgent, travelOffersAgent: TravelOffersAgent, @@ -35,17 +33,6 @@ class FeedsParser( } } - private val bestsellers: Option[FeedParser[Book]] = { - Configuration.commercial.magento.domain map { domain => - new FeedParser[Book] { - - val feedMetaData = BestsellersFeedMetaData(domain) - - def parse(feedContent: => Option[String]) = bestsellersAgent.refresh(feedMetaData, feedContent) - } - } - } - private val masterclasses: Option[FeedParser[Masterclass]] = { Configuration.commercial.masterclassesToken map { accessToken => new FeedParser[Masterclass] { @@ -79,7 +66,7 @@ class FeedsParser( } } - val all = Seq(jobs, bestsellers, masterclasses, liveEvents, travelOffers).flatten + val all = Seq(jobs, masterclasses, liveEvents, travelOffers).flatten } case class ParsedFeed[+T](contents: Seq[T], parseDuration: Duration) diff --git a/commercial/app/model/merchandise/Merchandise.scala b/commercial/app/model/merchandise/Merchandise.scala index 8856f5c253af..be8c8ff3e33c 100644 --- a/commercial/app/model/merchandise/Merchandise.scala +++ b/commercial/app/model/merchandise/Merchandise.scala @@ -21,20 +21,6 @@ import scala.xml.Node sealed trait Merchandise -case class Book( - title: String, - author: Option[String], - isbn: String, - price: Option[Double] = None, - offerPrice: Option[Double] = None, - description: Option[String] = None, - jacketUrl: Option[String], - buyUrl: Option[String] = None, - position: Option[Int] = None, - category: Option[String] = None, - keywordIdSuffixes: Seq[String] = Nil, -) extends Merchandise - case class Masterclass( id: String, name: String, @@ -160,7 +146,6 @@ object Merchandise { val merchandiseWrites: Writes[Merchandise] = new Writes[Merchandise] { def writes(m: Merchandise) = m match { - case b: Book => Json.toJson(b) case j: Job => Json.toJson(j) case m: Masterclass => Json.toJson(m) case m: Member => Json.toJson(m) @@ -175,39 +160,6 @@ object Merchandise { } } -object Book { - - private val authorReads = ((JsPath \ "author_firstname").readNullable[String] and - (JsPath \ "author_lastname").readNullable[String]).tupled.map { - case (optFirstName, optLastName) => - for { - firstName <- optFirstName - lastName <- optLastName - } yield s"$firstName $lastName" - } - - private def stringOrDoubleAsDouble(value: String): Reads[Option[Double]] = { - val path = JsPath \ value - path.readNullable[Double] orElse path.readNullable[String].map(_.map(_.toDouble)) - } - - implicit val bookReads: Reads[Book] = ( - (JsPath \ "name").read[String] and - authorReads and - (JsPath \ "isbn").read[String] and - stringOrDoubleAsDouble("regular_price_with_tax") and - stringOrDoubleAsDouble("final_price_with_tax") and - (JsPath \ "description").readNullable[String] and - (JsPath \ "images")(0).readNullable[String] and - (JsPath \ "product_url").readNullable[String] and - (JsPath \ "guardian_bestseller_rank").readNullable[String].map(_.map(_.toDouble.toInt)) and - ((JsPath \ "categories")(0) \ "name").readNullable[String] and - (JsPath \ "keywordIds").readNullable[Seq[String]].map(_ getOrElse Nil) - )(Book.apply _) - - implicit val bookWrites: Writes[Book] = Json.writes[Book] -} - object Masterclass { def fromEvent(event: Event): Option[Masterclass] = { diff --git a/commercial/app/model/merchandise/books/BestsellersAgent.scala b/commercial/app/model/merchandise/books/BestsellersAgent.scala deleted file mode 100644 index b7b67b08c1ef..000000000000 --- a/commercial/app/model/merchandise/books/BestsellersAgent.scala +++ /dev/null @@ -1,37 +0,0 @@ -package commercial.model.merchandise.books - -import commercial.model.Segment -import commercial.model.capi.Keyword -import commercial.model.feeds.{FeedMetaData, ParsedFeed} -import commercial.model.merchandise.{Book, MerchandiseAgent} - -import scala.concurrent.{ExecutionContext, Future} - -class BestsellersAgent(bookFinder: BookFinder) extends MerchandiseAgent[Book] { - - def getSpecificBook(isbn: String): Option[Book] = available find (_.isbn == isbn) - - def getSpecificBooks(isbns: Seq[String]): Seq[Book] = - (isbns flatMap bookFinder.findByIsbn).sortBy(book => isbns.indexOf(book.isbn)) - - def bestsellersTargetedAt(segment: Segment): Seq[Book] = { - val targetedBestsellers = available filter { book => - Keyword.idSuffixesIntersect(segment.context.keywords, book.keywordIdSuffixes) - } - lazy val defaultBestsellers = available filter (_.category.contains("General")) - val bestsellers = if (targetedBestsellers.isEmpty) defaultBestsellers else targetedBestsellers - bestsellers.filter(_.jacketUrl.nonEmpty).sortBy(_.position).take(10) - } - - def refresh(feedMetaData: FeedMetaData, feedContent: => Option[String])(implicit - executionContext: ExecutionContext, - ): Future[ParsedFeed[Book]] = { - val parsedFeed = MagentoBestsellersFeed.loadBestsellers(feedMetaData, feedContent) - - for (feed <- parsedFeed) { - updateAvailableMerchandise(feed.contents) - } - - parsedFeed - } -} diff --git a/commercial/app/model/merchandise/books/BookFinder.scala b/commercial/app/model/merchandise/books/BookFinder.scala deleted file mode 100644 index f7d6c775f63a..000000000000 --- a/commercial/app/model/merchandise/books/BookFinder.scala +++ /dev/null @@ -1,139 +0,0 @@ -package commercial.model.merchandise.books - -import org.apache.pekko.actor.{ActorSystem => PekkoActorSystem} -import org.apache.pekko.pattern.CircuitBreaker -import org.apache.pekko.util.Timeout -import commercial.model.feeds.{FeedParseException, FeedReadException, FeedReader, FeedRequest} -import commercial.model.merchandise.Book -import common.{Box, GuLogging} -import conf.Configuration -import play.api.libs.json._ -import play.api.libs.oauth.{ConsumerKey, OAuthCalculator, RequestToken} -import play.api.libs.ws.{WSClient, WSSignatureCalculator} - -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success} - -class BookFinder(pekkoActorSystem: PekkoActorSystem, magentoService: MagentoService) extends GuLogging { - - private implicit val bookActorExecutionContext: ExecutionContext = - pekkoActorSystem.dispatchers.lookup("pekko.actor.book-lookup") - private implicit val bookActorTimeout: Timeout = 0.2.seconds - private implicit val magentoServiceImplicit = magentoService - - def findByIsbn(isbn: String): Option[Book] = BookAgent.get(isbn) map { _.as[Book] } -} - -object BookAgent extends GuLogging { - - private lazy val cache = Box(Map.empty[String, JsValue]) - - def get( - isbn: String, - )(implicit magentoService: MagentoService, executionContext: ExecutionContext): Option[JsValue] = { - - val bookJson: Option[JsValue] = cache.get().get(isbn) - - if (bookJson.isEmpty) { - magentoService.findByIsbn(isbn) onComplete { - case Failure(e) => log.error("Magento lookup failed.", e) - case Success(None) => log.warn(s"Magento unable to find book for $isbn.") - case Success(Some(json: JsValue)) => cache alter { _ + (isbn -> json) } - } - } - - bookJson - - } -} - -class MagentoService(pekkoActorSystem: PekkoActorSystem, wsClient: WSClient) extends GuLogging { - - private case class MagentoProperties(oauth: WSSignatureCalculator, urlPrefix: String) - - private val feedReader = new FeedReader(wsClient) - - private val magentoProperties = { - for { - domain <- Configuration.commercial.magento.domain - path <- Configuration.commercial.magento.isbnLookupPath - consumerKey <- Configuration.commercial.magento.consumerKey - consumerSecret <- Configuration.commercial.magento.consumerSecret - token <- Configuration.commercial.magento.accessToken - tokenSecret <- Configuration.commercial.magento.accessTokenSecret - } yield MagentoProperties( - oauth = OAuthCalculator( - consumerKey = ConsumerKey(consumerKey, consumerSecret), - token = RequestToken(token, tokenSecret), - ), - urlPrefix = s"https://$domain/$path", - ) - } - - private implicit val bookLookupExecutionContext: ExecutionContext = - pekkoActorSystem.dispatchers.lookup("pekko.actor.book-lookup") - - private final val circuitBreaker = new CircuitBreaker( - scheduler = pekkoActorSystem.scheduler, - maxFailures = 5, - callTimeout = 3.seconds, - resetTimeout = 5.minutes, - ) - - circuitBreaker.onOpen( - log.error("Book lookup circuit breaker tripped: Open"), - ) - - circuitBreaker.onHalfOpen( - log.info("Book lookup circuit breaker tentatively trying again: Half Open"), - ) - - circuitBreaker.onClose( - log.info("Book lookup circuit breaker safe: Closed."), - ) - - def findByIsbn(isbn: String): Future[Option[JsValue]] = { - - def lookup(isbn: String): Future[Option[JsValue]] = { - - val result = magentoProperties map { props => - val request = FeedRequest( - feedName = "Book Lookup", - url = s"${props.urlPrefix}/$isbn", - responseEncoding = "utf-8", - timeout = 4.seconds, - ) - - log.info(s"Looking up book with ISBN $isbn ...") - - feedReader.read(request, signature = Some(props.oauth), validResponseStatuses = Seq(200, 404)) { responseBody => - val bookJson = Json.parse(responseBody) - bookJson.validate[Book] match { - case JsError(e) => - MagentoException(bookJson) match { - case Some(me) if me.code == 404 => - log.warn(s"MagentoService could not find isbn $isbn") - None - case Some(me) => - log.warn(s"MegentoException: $me") - throw FeedReadException(request, me.code, me.message) - case None => - val jsonErr = JsError.toJson(e).toString() - log.warn(s"Unable to validate Book: $jsonErr") - throw FeedParseException(request, jsonErr) - } - case JsSuccess(_, _) => Some(bookJson) - } - } - } - - result getOrElse { - log.warn("MagentoService is not configured") - Future.successful(None) - } - } - - circuitBreaker.withCircuitBreaker(lookup(isbn)) - } -} diff --git a/commercial/app/model/merchandise/books/MagentoBestsellersFeed.scala b/commercial/app/model/merchandise/books/MagentoBestsellersFeed.scala deleted file mode 100644 index e22f682b9186..000000000000 --- a/commercial/app/model/merchandise/books/MagentoBestsellersFeed.scala +++ /dev/null @@ -1,58 +0,0 @@ -package commercial.model.merchandise.books - -import java.lang.System._ - -import commercial.model.OptString -import commercial.model.feeds._ -import common.GuLogging -import commercial.model.merchandise.Book - -import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.duration._ -import scala.util.control.NonFatal -import scala.xml.{Elem, XML} - -object MagentoBestsellersFeed extends GuLogging { - - def parse(xml: Elem): Seq[Book] = { - xml \ "Entry" map { entry => - val book = entry \ "book" - - def getPrice(eltName: String): Option[Double] = Some((book \ eltName).text).map(_.stripPrefix("£").toDouble) - - Book( - title = (book \ "title").text, - author = OptString((book \ "author").text), - isbn = (book \ "isbn").text, - price = getPrice("price"), - offerPrice = getPrice("offerprice"), - description = OptString((book \ "description").text), - jacketUrl = (book \ "jacketurl").headOption.map(node => - s"http:${node.text}" - .replace("http://images.bertrams.com/ProductImages/services/GetImage", "http://c.guim.co.uk/books"), - ), - buyUrl = Some((book \ "bookurl").text), - position = Some((entry \ "Position").text).map(_.toInt), - Some("General"), - Nil, - ) - } - } - - def loadBestsellers(feedMetaData: FeedMetaData, feedContent: => Option[String])(implicit - executionContext: ExecutionContext, - ): Future[ParsedFeed[Book]] = { - - val feedName = feedMetaData.name - - val start = currentTimeMillis - feedContent map { body => - val parsed = parse(XML.loadString(body)).map { book => - book.copy(jacketUrl = book.jacketUrl.map(_.stripPrefix("http:"))) - } - Future(ParsedFeed(parsed, Duration(currentTimeMillis - start, MILLISECONDS))) - } getOrElse { - Future.failed(MissingFeedException(feedName)) - } - } -} diff --git a/commercial/app/model/merchandise/books/MagentoException.scala b/commercial/app/model/merchandise/books/MagentoException.scala deleted file mode 100644 index 8b45f9cec5b2..000000000000 --- a/commercial/app/model/merchandise/books/MagentoException.scala +++ /dev/null @@ -1,28 +0,0 @@ -package commercial.model.merchandise.books - -import common.GuLogging -import play.api.libs.json.{JsError, JsSuccess, JsValue} - -case class MagentoException(code: Int, message: String) - -object MagentoException extends GuLogging { - - def apply(json: JsValue): Option[MagentoException] = { - val error = (json \ "messages" \ "error")(0) - - val parseResult = for { - code <- (error \ "code").validate[Int] - message <- (error \ "message").validate[String] - } yield { - MagentoException(code, message) - } - - parseResult match { - case JsError(e) => - log.error(s"MagentoException failed to parse $json: ${JsError.toJson(e).toString()}") - None - case JsSuccess(magentoException, _) => Some(magentoException) - } - } - -} diff --git a/commercial/conf/routes b/commercial/conf/routes index c8f803906647..f3241869ac99 100644 --- a/commercial/conf/routes +++ b/commercial/conf/routes @@ -13,10 +13,6 @@ GET /commercial/travel/api/offers.json comm # Job merchandising components GET /commercial/jobs/api/jobs.json commercial.controllers.JobsController.getJobs -# Book merchandising components -GET /commercial/books/api/book.json commercial.controllers.BookOffersController.getBook -GET /commercial/books/api/books.json commercial.controllers.BookOffersController.getBooks - # Live events merchandising components GET /commercial/api/liveevent.json commercial.controllers.LiveEventsController.getLiveEvent diff --git a/commercial/test/model/books/BookTest.scala b/commercial/test/model/books/BookTest.scala deleted file mode 100644 index c3a6dd4ff7ee..000000000000 --- a/commercial/test/model/books/BookTest.scala +++ /dev/null @@ -1,54 +0,0 @@ -package commercial.model.merchandise.books - -import commercial.model.merchandise.Book -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.DoNotDiscover -import play.api.libs.json.Json -import test.ConfiguredTestSuite - -@DoNotDiscover class BookTest extends AnyFlatSpec with Matchers with ConfiguredTestSuite { - - private val json = Json.parse( - """{"sku":"9780001712768", - |"isbn":"9780001712768", - |"name":"In a People House", - |"author_firstname":"Dr", - |"author_lastname":"Seuss", - |"bestseller_rank":"223.0000", - |"guardian_bestseller_rank":"222.0000", - |"categories":[ - |{"name":"Picture books","bic":"YYTG,YBC"}, - |{"name":"All fiction and poetry","bic": - |"FC,FA,FF,FH,FJ,FK,FL,FM,FP,FQ,FR,FT,FV,FW,FX,FY,FZ,FG,CTC,FBC,FBN,FBV,FGB,FGC,FGH,FGL,FGM,FGN,FGQ,FGU,FGV,FGW,FNB,FND,FNG,FNS,FS"} - |], - |"images":[ - |"http:\/\/guardianbookshop.staging.lab.co.uk\/image\/9df78eab33525d08d6e5fb8d27136e95\/media2\/73e70a25faab42aa1b411b8b59382416.jpg" - |], - |"product_url":"http:\/\/guardianbookshop.staging.lab.co.uk\/index.php\/in-a-people-house.html", - |"regular_price_with_tax":"5.0915", - |"regular_price_without_tax":"5.0915", - |"final_price_with_tax":"5.0914", - |"final_price_without_tax":5.0914}""".stripMargin, - ) - - "Book" should "create a Book from json" in { - json.validate[Book].asOpt shouldBe Some( - Book( - title = "In a People House", - author = Some("Dr Seuss"), - isbn = "9780001712768", - price = Some(5.0915), - offerPrice = Some(5.0914), - description = None, - jacketUrl = Some( - "http://guardianbookshop.staging.lab.co.uk/image/9df78eab33525d08d6e5fb8d27136e95/media2/73e70a25faab42aa1b411b8b59382416.jpg", - ), - buyUrl = Some("http://guardianbookshop.staging.lab.co.uk/index.php/in-a-people-house.html"), - position = Some(222), - category = Some("Picture books"), - keywordIdSuffixes = Nil, - ), - ) - } -} diff --git a/commercial/test/model/books/MagentoBestsellersFeedTest.scala b/commercial/test/model/books/MagentoBestsellersFeedTest.scala deleted file mode 100644 index 6061990dd39f..000000000000 --- a/commercial/test/model/books/MagentoBestsellersFeedTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -package commercial.model.merchandise.books - -import commercial.model.merchandise.Book -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.DoNotDiscover -import test.ConfiguredTestSuite - -import scala.xml.XML - -@DoNotDiscover class MagentoBestsellersFeedTest extends AnyFlatSpec with Matchers with ConfiguredTestSuite { - - private val xmlStr = - """General - |1Guardian Quick Crosswords 5 & 6503849511323813.988.00//c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=5038495113238http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=5038495113238 - |2Twelve Years a SlaveSolomon Northup97801413938277.996.39Solomon Northup is a free man, living in New York. Then he is kidnapped and sold into slavery. Drugged, beaten, given a new name and transported away from his wife and children to a Louisiana cotton plantation, Solomon will die if he reveals his true identity.//c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9780141393827http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9780141393827 - |3How to Be AloneSara Maitland97802307680867.996.39Learn how to enjoy solitude and find happiness without others//c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9780230768086http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9780230768086 - |41941: The Year That Keeps ReturningSlavko Goldstein978159017673319.9915.49//c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9781590176733http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9781590176733 - |5Examined LifeStephen Grosz97800995490318.996.99Reveals how the art of insight can illuminate the most complicated, confounding and human of experiences. This title includes stories about our everyday lives: they are about the people we love and the lies that we tell; the changes we bear, and the grief.//c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9780099549031http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9780099549031 - |""".stripMargin - - "parse" should "parse books from xml feed" in { - val books = MagentoBestsellersFeed.parse(XML.loadString(xmlStr)) - - books should be( - Seq( - Book( - "Guardian Quick Crosswords 5 & 6", - None, - "5038495113238", - Some(13.98), - Some(8.0), - None, - Some("http://c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=5038495113238"), - Some("http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=5038495113238"), - Some(1), - Some("General"), - Nil, - ), - Book( - "Twelve Years a Slave", - Some("Solomon Northup"), - "9780141393827", - Some(7.99), - Some(6.39), - Some( - "Solomon Northup is a free man, living in New York. Then he is kidnapped and sold into slavery. Drugged, beaten, given a new name and transported away from his wife and children to a Louisiana cotton plantation, Solomon will die if he reveals his true identity.", - ), - Some("http://c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9780141393827"), - Some("http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9780141393827"), - Some(2), - Some("General"), - Nil, - ), - Book( - "How to Be Alone", - Some("Sara Maitland"), - "9780230768086", - Some(7.99), - Some(6.39), - Some("Learn how to enjoy solitude and find happiness without others"), - Some("http://c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9780230768086"), - Some("http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9780230768086"), - Some(3), - Some("General"), - Nil, - ), - Book( - "1941: The Year That Keeps Returning", - Some("Slavko Goldstein"), - "9781590176733", - Some(19.99), - Some(15.49), - None, - Some("http://c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9781590176733"), - Some("http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9781590176733"), - Some(4), - Some("General"), - Nil, - ), - Book( - "Examined Life", - Some("Stephen Grosz"), - "9780099549031", - Some(8.99), - Some(6.99), - Some( - "Reveals how the art of insight can illuminate the most complicated, confounding and human of experiences. This title includes stories about our everyday lives: they are about the people we love and the lies that we tell; the changes we bear, and the grief.", - ), - Some("http://c.guim.co.uk/books?Source=BERT&Quality=WEB&Component=FRONTCOVER&EAN13=9780099549031"), - Some("http://www.guardianbookshop.co.uk/BerteShopWeb/viewProduct.do?ISBN=9780099549031"), - Some(5), - Some("General"), - Nil, - ), - ), - ) - } -} diff --git a/commercial/test/model/books/MagentoExceptionTest.scala b/commercial/test/model/books/MagentoExceptionTest.scala deleted file mode 100644 index c0a4339e2d19..000000000000 --- a/commercial/test/model/books/MagentoExceptionTest.scala +++ /dev/null @@ -1,16 +0,0 @@ -package commercial.model.merchandise.books - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.DoNotDiscover -import org.scalatest.matchers.should.Matchers -import play.api.libs.json.Json -import test.ConfiguredTestSuite - -@DoNotDiscover class MagentoExceptionTest extends AnyFlatSpec with Matchers with ConfiguredTestSuite { - - "apply" should "create a MagentoException from json" in { - val json = Json.parse("""{"messages":{"error":[{"code":404,"message":"Resource not found."}]}}""") - MagentoException(json) shouldBe Some(MagentoException(404, "Resource not found.")) - } - -} diff --git a/commercial/test/test/CommercialTestSuite.scala b/commercial/test/test/CommercialTestSuite.scala index 456354ac890f..cc5a3794a080 100644 --- a/commercial/test/test/CommercialTestSuite.scala +++ b/commercial/test/test/CommercialTestSuite.scala @@ -1,18 +1,15 @@ package commercial.test import commercial.model.capi.LookupTest -import commercial.model.merchandise.{books, events, jobs} +import commercial.model.merchandise.{events, jobs} import org.scalatest.Suites import test.SingleServerSuite class CommercialTestSuite extends Suites( - new books.MagentoBestsellersFeedTest, - new books.MagentoExceptionTest, new jobs.JobTest, new events.EventbriteMasterclassFeedParsingTest, new events.SingleEventbriteMasterclassParsingTest, new LookupTest, - new books.BookTest, ) with SingleServerSuite {} diff --git a/common/app/common/configuration.scala b/common/app/common/configuration.scala index e4fca80d2eff..9798ad8a48bc 100644 --- a/common/app/common/configuration.scala +++ b/common/app/common/configuration.scala @@ -521,16 +521,6 @@ class GuardianConfiguration extends GuLogging { lazy val liveEventsMembershipUrl = "https://membership.theguardian.com/events.json" lazy val jobsUrl = configuration.getStringProperty("jobs.api.url") - object magento { - lazy val domain = configuration.getStringProperty("magento.domain") - lazy val consumerKey = configuration.getStringProperty("magento.consumer.key") - lazy val consumerSecret = configuration.getStringProperty("magento.consumer.secret") - lazy val accessToken = configuration.getStringProperty("magento.access.token.key") - lazy val accessTokenSecret = configuration.getStringProperty("magento.access.token.secret") - lazy val authorizationPath = configuration.getStringProperty("magento.auth.path") - lazy val isbnLookupPath = configuration.getStringProperty("magento.isbn.lookup.path") - } - lazy val adOpsTeam = configuration.getStringProperty("email.adOpsTeam") lazy val adOpsAuTeam = configuration.getStringProperty("email.adOpsTeamAu") lazy val adOpsUsTeam = configuration.getStringProperty("email.adOpsTeamUs") diff --git a/common/app/dev/DevParametersHttpRequestHandler.scala b/common/app/dev/DevParametersHttpRequestHandler.scala index 7b0d77666977..c66e03ef4cc0 100644 --- a/common/app/dev/DevParametersHttpRequestHandler.scala +++ b/common/app/dev/DevParametersHttpRequestHandler.scala @@ -36,8 +36,6 @@ class DevParametersHttpRequestHandler( "test", // used for integration tests "CMP", // External campaign parameter for Omniture js "INTCMP", // Internal campaign parameter for Omniture js - "oauth_token", // for generating Magento tokens for bookshop service - "oauth_verifier", // for generating Magento tokens for bookshop service "query", // testing the weather locations endpoint "rel", // used by browsersync "pageSize", diff --git a/dev-build/app/AppLoader.scala b/dev-build/app/AppLoader.scala index 0ebec672af6b..70e0ee79465d 100644 --- a/dev-build/app/AppLoader.scala +++ b/dev-build/app/AppLoader.scala @@ -14,7 +14,6 @@ import conf.FootballLifecycle import conf.switches.SwitchboardLifecycle import contentapi.{CapiHttpClient, ContentApiClient, HttpClient, SectionsLookUpLifecycle} import controllers._ -import controllers.commercial.magento.{AccessTokenGenerator, ApiSandbox} import cricket.conf.CricketLifecycle import cricket.controllers.CricketControllers import dev.DevAssetsController @@ -55,8 +54,6 @@ trait Controllers def newsletterSignupAgent: NewsletterSignupAgent - lazy val accessTokenGenerator = wire[AccessTokenGenerator] - lazy val apiSandbox = wire[ApiSandbox] lazy val devAssetsController = wire[DevAssetsController] lazy val emailSignupController = wire[EmailSignupController] lazy val surveyPageController = wire[SurveyPageController] diff --git a/dev-build/app/controllers/commercial/magento/AccessTokenGenerator.scala b/dev-build/app/controllers/commercial/magento/AccessTokenGenerator.scala deleted file mode 100644 index 0b012610d086..000000000000 --- a/dev-build/app/controllers/commercial/magento/AccessTokenGenerator.scala +++ /dev/null @@ -1,71 +0,0 @@ -package controllers.commercial.magento - -import conf.Configuration -import model.NoCache -import play.api.libs.oauth._ -import play.api.mvc._ - -/** - * For one-off generation of Magento access tokens. - * The bookshop is a Magento service. - */ -class AccessTokenGenerator(val controllerComponents: ControllerComponents) extends BaseController { - - private lazy val authService = for { - domain <- Configuration.commercial.magento.domain - consumerKey <- Configuration.commercial.magento.consumerKey - consumerSecret <- Configuration.commercial.magento.consumerSecret - authorizationPath <- Configuration.commercial.magento.authorizationPath - } yield { - OAuth( - ServiceInfo( - requestTokenURL = s"https://$domain/oauth/initiate", - accessTokenURL = s"https://$domain/oauth/token", - authorizationURL = s"https://$domain/$authorizationPath", - key = ConsumerKey(consumerKey, consumerSecret), - ), - use10a = true, - ) - } - - private val unavailable: Result = ServiceUnavailable("Missing properties.") - - def generate: Action[AnyContent] = - Action { implicit request => - def genRequestToken(): Result = { - authService.fold(unavailable) { auth => - val callbackUrl = routes.AccessTokenGenerator.generate.absoluteURL() - auth.retrieveRequestToken(callbackUrl) match { - case Right(t) => - Redirect(auth.redirectUrl(t.token)).withSession("token" -> t.token, "secret" -> t.secret) - case Left(e) => throw e - } - } - } - - def requestTokenFromSession: RequestToken = { - (for { - token <- request.session.get("token") - secret <- request.session.get("secret") - } yield { - RequestToken(token, secret) - }).get - } - - def genAccessToken(tokenPair: RequestToken, verifier: String): Result = { - authService.fold(unavailable) { auth => - auth.retrieveAccessToken(tokenPair, verifier) match { - case Left(e) => throw e - case Right(accessToken) => - Ok(s"Token: ${accessToken.token}\nSecret: ${accessToken.secret}").withSession() - } - } - } - - val result = request.getQueryString("oauth_verifier") map { verifier => - genAccessToken(requestTokenFromSession, verifier) - } getOrElse genRequestToken() - - NoCache(result) - } -} diff --git a/dev-build/app/controllers/commercial/magento/ApiSandbox.scala b/dev-build/app/controllers/commercial/magento/ApiSandbox.scala deleted file mode 100644 index 6c9cf5c06589..000000000000 --- a/dev-build/app/controllers/commercial/magento/ApiSandbox.scala +++ /dev/null @@ -1,54 +0,0 @@ -package controllers.commercial.magento - -import common.ImplicitControllerExecutionContext -import conf.Configuration.commercial.magento -import model.NoCache -import play.api.libs.oauth.{ConsumerKey, OAuthCalculator, RequestToken} -import play.api.libs.ws.WSClient -import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} - -/** - * This allows us to check the content of protected Magento endpoints. - * - * See: - * http://www.magentocommerce.com/api/rest/Resources/resources.html - * http://www.magentocommerce.com/api/rest/get_filters.html - */ -class ApiSandbox(wsClient: WSClient, val controllerComponents: ControllerComponents) - extends BaseController - with ImplicitControllerExecutionContext { - - private val domain = magento.domain.getOrElse( - throw new RuntimeException("Unable to get [magento.domain] property. Is it set in the configuration?"), - ) - private val oauth = { - val key = ConsumerKey(magento.consumerKey.get, magento.consumerSecret.get) - val accessToken = RequestToken( - magento.accessToken.getOrElse( - throw new RuntimeException( - "update frontend.properties in your .gu folder - change \"magento.access.token\" to \"magento.access.token.key\"", - ), - ), - magento.accessTokenSecret.get, - ) - OAuthCalculator(key, accessToken) - } - - def getResource(path: String): Action[AnyContent] = - Action.async { implicit request => - wsClient - .url(s"http://$domain/$path") - .sign(oauth) - .get() - .map(result => NoCache(Ok(result.body))) - } - - def getBooks(csvIsbns: String): Action[AnyContent] = { - val isbns = csvIsbns split "," - val path = isbns.zipWithIndex.foldLeft("api/rest/products?filter[1][attribute]=isbn") { - case (soFar, (isbn, i)) => s"$soFar&filter[1][in][${i + 1}]=$isbn" - } - getResource(path) - } - -} diff --git a/dev-build/conf/routes b/dev-build/conf/routes index 8e1e323eeff9..eaedbc8234e4 100644 --- a/dev-build/conf/routes +++ b/dev-build/conf/routes @@ -259,8 +259,6 @@ GET /admin/football/api/squad/:teamId # Commercial GET /commercial/travel/api/offers.json commercial.controllers.TravelOffersController.getTravel GET /commercial/jobs/api/jobs.json commercial.controllers.JobsController.getJobs -GET /commercial/books/api/book.json commercial.controllers.BookOffersController.getBook -GET /commercial/books/api/books.json commercial.controllers.BookOffersController.getBooks GET /commercial/api/capi-single.json commercial.controllers.ContentApiOffersController.nativeJson GET /commercial/api/capi-multiple.json commercial.controllers.ContentApiOffersController.nativeJsonMulti GET /commercial/api/traffic-driver.json commercial.controllers.TrafficDriverController.renderJson() @@ -310,11 +308,6 @@ POST /commercial/adops/ads-txt GET /commercial/adops/app-ads-txt controllers.admin.commercial.AdsDotTextEditController.renderAppAdsDotText() POST /commercial/adops/app-ads-txt controllers.admin.commercial.AdsDotTextEditController.postAppAdsDotText() -# dev-build only -GET /commercial/magento/token controllers.commercial.magento.AccessTokenGenerator.generate -GET /commercial/magento/resource controllers.commercial.magento.ApiSandbox.getResource(t: String) -GET /commercial/magento/books controllers.commercial.magento.ApiSandbox.getBooks(t: String) - # AMP GET /most-read-mf2.json controllers.MostPopularController.renderPopularMicroformat2() GET /related-mf2/*path.json controllers.RelatedController.renderMf2(path)