@@ -6,6 +6,11 @@ import com.gu.contentapi.client.model.v1.EmbedTracksType.DoesNotTrack
66import com .gu .contentapi .client .model .v1 .{
77 EmbedTracking ,
88 LinkType ,
9+ ProductDisplayType ,
10+ ProductElementFields ,
11+ ProductCTA => ApiProductCta ,
12+ ProductCustomAttribute => ApiProductCustomAttribute ,
13+ ProductImage => ApiProductImage ,
914 SponsorshipType ,
1015 TimelineElementFields ,
1116 WitnessElementFields ,
@@ -510,6 +515,48 @@ object LinkBlockElement {
510515 implicit val LinkBlockElementWrites : Writes [LinkBlockElement ] = Json .writes[LinkBlockElement ]
511516}
512517
518+ case class ProductImage (
519+ url : String ,
520+ caption : String ,
521+ height : Int ,
522+ width : Int ,
523+ alt : String ,
524+ credit : String ,
525+ displayCredit : Boolean ,
526+ )
527+ case class ProductCustomAttribute (
528+ name : String ,
529+ value : String ,
530+ )
531+ case class ProductCta (
532+ text : String ,
533+ price : String ,
534+ retailer : String ,
535+ url : String ,
536+ )
537+ case class ProductBlockElement (
538+ productName : String ,
539+ brandName : String ,
540+ primaryHeadingHtml : String ,
541+ secondaryHeadingHtml : String ,
542+ starRating : String ,
543+ productCtas : List [ProductCta ],
544+ customAttributes : List [ProductCustomAttribute ],
545+ image : Option [ProductImage ],
546+ content : Seq [PageElement ],
547+ displayType : ProductDisplayType ,
548+ ) extends PageElement
549+ object ProductBlockElement {
550+ implicit val ProductBlockElementImageWrites : Writes [ProductImage ] = Json .writes[ProductImage ]
551+ implicit val ProductBlockElementCTAWrites : Writes [ProductCta ] = Json .writes[ProductCta ]
552+ implicit val ProductBlockElementCustomAttributeWrites : Writes [ProductCustomAttribute ] =
553+ Json .writes[ProductCustomAttribute ]
554+ implicit val ProductBlockElementDisplayTypeWrites : Writes [ProductDisplayType ] = Writes { displayType =>
555+ JsString (displayType.name)
556+ }
557+ implicit val ProductBlockElementWrites : Writes [ProductBlockElement ] = Json .writes[ProductBlockElement ]
558+ }
559+
513560case class QABlockElement (id : String , title : String , img : Option [String ], html : String , credit : String )
514561 extends PageElement
515562object QABlockElement {
@@ -897,6 +944,7 @@ object PageElement {
897944 case _ : ListBlockElement => true
898945 case _ : TimelineBlockElement => true
899946 case _ : LinkBlockElement => true
947+ case _ : ProductBlockElement => true
900948
901949 // TODO we should quick fail here for these rather than pointlessly go to DCR
902950 case table : TableBlockElement if table.isMandatory.exists(identity) => true
@@ -1534,6 +1582,23 @@ object PageElement {
15341582 )
15351583 }.toList
15361584
1585+ case Product =>
1586+ element.productTypeData.map { productTypeData =>
1587+ makeProduct(
1588+ addAffiliateLinks,
1589+ pageUrl,
1590+ atoms,
1591+ isImmersive,
1592+ campaigns,
1593+ calloutsUrl,
1594+ edition,
1595+ webPublicationDate,
1596+ productTypeData,
1597+ isGallery,
1598+ isTheFilterUS,
1599+ )
1600+ }.toList
1601+
15371602 case EnumUnknownElementType (f) => List (UnknownBlockElement (None ))
15381603 case _ => Nil
15391604 }
@@ -1643,6 +1708,112 @@ object PageElement {
16431708 )
16441709 }
16451710
1711+ private def makeProduct (
1712+ addAffiliateLinks : Boolean ,
1713+ pageUrl : String ,
1714+ atoms : Iterable [Atom ],
1715+ isImmersive : Boolean ,
1716+ campaigns : Option [JsValue ],
1717+ calloutsUrl : Option [String ],
1718+ edition : Edition ,
1719+ webPublicationDate : DateTime ,
1720+ product : ProductElementFields ,
1721+ isGallery : Boolean ,
1722+ isTheFilterUS : Boolean ,
1723+ ) = {
1724+
1725+ def createProductCta (
1726+ cta : ApiProductCta ,
1727+ pageUrl : String ,
1728+ addAffiliateLinks : Boolean ,
1729+ isTheFilterUS : Boolean ,
1730+ ): Option [ProductCta ] = {
1731+ for {
1732+ // URL must exist and be non-empty
1733+ url <- AffiliateLinksCleaner
1734+ .replaceUrlInLink(cta.url, pageUrl, addAffiliateLinks, isTheFilterUS)
1735+ .filter(_.nonEmpty)
1736+
1737+ // Must have either non-empty text, or both non-empty price & retailer
1738+ if cta.text.exists(_.nonEmpty) ||
1739+ (cta.price.exists(_.nonEmpty) && cta.retailer.exists(_.nonEmpty))
1740+ } yield ProductCta (
1741+ text = cta.text.getOrElse(" " ),
1742+ price = cta.price.getOrElse(" " ),
1743+ retailer = cta.retailer.getOrElse(" " ),
1744+ url = url,
1745+ )
1746+ }
1747+
1748+ def createProductCustomAttribute (apiCustomAttribute : ApiProductCustomAttribute ): Option [ProductCustomAttribute ] = {
1749+ for {
1750+ name <- apiCustomAttribute.name if name.nonEmpty
1751+ value <- apiCustomAttribute.value if value.nonEmpty
1752+ } yield ProductCustomAttribute (
1753+ name = name,
1754+ value = value,
1755+ )
1756+ }
1757+
1758+ def createProductImage (apiImage : ApiProductImage ): Option [ProductImage ] = {
1759+ for {
1760+ url <- apiImage.file if url.nonEmpty
1761+ height <- apiImage.height
1762+ width <- apiImage.width
1763+ displayCredit <- apiImage.displayCredit
1764+ credit <- apiImage.credit
1765+ alt <- apiImage.alt
1766+ } yield ProductImage (
1767+ url = url,
1768+ caption = apiImage.caption.getOrElse(" " ),
1769+ credit = credit,
1770+ height = height,
1771+ width = width,
1772+ displayCredit = displayCredit,
1773+ alt = alt,
1774+ )
1775+ }
1776+
1777+ ProductBlockElement (
1778+ content = product.content
1779+ .getOrElse(List ())
1780+ .flatMap { element =>
1781+ PageElement .make(
1782+ element,
1783+ addAffiliateLinks,
1784+ pageUrl,
1785+ atoms,
1786+ isMainBlock = false ,
1787+ isImmersive,
1788+ campaigns,
1789+ calloutsUrl,
1790+ overrideImage = None ,
1791+ edition,
1792+ webPublicationDate,
1793+ isGallery,
1794+ isTheFilterUS,
1795+ )
1796+ }
1797+ .toSeq,
1798+ productName = product.productName.getOrElse(" " ),
1799+ brandName = product.brandName.getOrElse(" " ),
1800+ primaryHeadingHtml = product.primaryHeading.getOrElse(" " ),
1801+ secondaryHeadingHtml = product.secondaryHeading.getOrElse(" " ),
1802+ starRating = product.starRating.getOrElse(" none-selected" ),
1803+ productCtas = product.productCtas
1804+ .getOrElse(Seq .empty)
1805+ .flatMap(cta => createProductCta(cta, pageUrl, addAffiliateLinks, isTheFilterUS))
1806+ .toList,
1807+ customAttributes = product.customAttributes
1808+ .getOrElse(Seq .empty)
1809+ .flatMap(apiAttr => createProductCustomAttribute(apiAttr))
1810+ .toList,
1811+ image = product.image.flatMap(apiImage => createProductImage(apiImage)),
1812+ displayType = product.displayType,
1813+ )
1814+
1815+ }
1816+
16461817 private [this ] def ensureHTTPS (url : String ): String = {
16471818 val http = " http://"
16481819
0 commit comments