app/com/gu/memsub/subsv2/services/CatalogService.scala (121 lines of code) (raw):
package com.gu.memsub.subsv2.services
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.model.GetObjectRequest
import com.gu.aws.AwsS3
import com.gu.memsub.BillingPeriod._
import com.gu.memsub.Subscription.ProductRatePlanId
import com.gu.memsub._
import com.gu.memsub.subsv2.CatalogPlan._
import com.gu.memsub.subsv2._
import com.gu.memsub.subsv2.reads.CatJsonReads._
import com.gu.memsub.subsv2.reads.CatPlanReads
import com.gu.memsub.subsv2.reads.CatPlanReads._
import com.gu.memsub.subsv2.reads.ChargeListReads.{ProductIds, _}
import play.api.libs.json.{Reads => JsReads, _}
import scalaz.Validation.FlatMap._
import scalaz.syntax.monad._
import scalaz.syntax.nel._
import scalaz.syntax.std.either._
import scalaz.{EitherT, Monad, NonEmptyList, Validation, ValidationNel, \/}
import scalaz.syntax.apply.ToApplyOps
object FetchCatalog {
def fromS3[M[_] : Monad](zuoraEnvironment: String, s3Client: AmazonS3 = AwsS3.client): M[String \/ JsValue] = {
val catalogRequest = new GetObjectRequest("gu-zuora-catalog", s"PROD/Zuora-$zuoraEnvironment/catalog.json")
AwsS3.fetchJson(s3Client, catalogRequest).point[M]
}
}
class CatalogService[M[_] : Monad](productIds: ProductIds, fetchCatalog: M[String \/ JsValue], unsafeGet: M[Catalog] => Catalog, stage: String) {
case class ErrorReport(problem: String, underlying: Map[ProductRatePlanId, String]) {
override def toString = s"\n$problem\n\n--- HERE IS WHAT WE TRIED --\n" +
underlying.foldLeft("") { case (a, (id, str)) => s"$a${id.get}: $str\n" }
}
/*
frontend id is to prioritise searching for tagged rateplans so we can look for 6 for 6 or quarterly and find the tagged one, but since most of the others won't
be tagged, we need to fall back for untagged ones.
in time, I expect them all to be requested by the tag, so we can make frontend id non optional
in time, I also expect they will all be tagged in zuora, so we don't need to then try untagged ones
*/
def one[P <: CatalogPlan[Product, ChargeList, Status]](plans: List[CatalogZuoraPlan], name: String, frontendId: FrontendId)(implicit p: CatPlanReads[P]): Validation[NonEmptyList[String], P] = {
many(plans.filter(_.frontendId.contains(frontendId)), name).flatMap {
case a if a.size == 1 => Validation.s(a.head)
case e => Validation.failureNel(s"Too many plans! $e")
}
}
def many[P <: CatalogPlan[Product, ChargeList, Status]](plans: List[CatalogZuoraPlan], name: String)(implicit p: CatPlanReads[P]): ValidationNel[String, NonEmptyList[P]] = {
val parsed: List[(ProductRatePlanId, ValidationNel[String, P])] =
plans.map(plan => (plan.id, p.read(productIds, plan)))
val failures = parsed.filter { case (_, validation) => validation.isFailure }
.map { case (id, err) => (id, err.swap.map(_.list.toList).getOrElse(List.empty).mkString(", ")) }
.toMap
parsed.map { case (_, validation) => validation }
.flatMap(_.toList) match {
case n :: ns => Validation.success[NonEmptyList[String], NonEmptyList[P]](NonEmptyList.fromSeq(n, ns))
case Nil => Validation.failureNel(ErrorReport(s"Failed to find $name in $stage", failures).toString)
}
}
def joinUp: M[String \/ List[CatalogZuoraPlan]] = (for {
catalog <- EitherT[String, M, JsValue](fetchCatalog)
catalogPlans <- EitherT[String, M, List[CatalogZuoraPlan]](Json.fromJson[List[CatalogZuoraPlan]](catalog).asEither.disjunction.leftMap(_.toString).point[M])
} yield catalogPlans).run
def validatePlans(plans: List[CatalogZuoraPlan]): Validation[NonEmptyList[String], Catalog] = for {
digipack <- (
one[Digipack[Month.type]](plans, "Digipack month", FrontendId.Monthly) |@|
one[Digipack[Quarter.type]](plans, "Digipack quarter", FrontendId.Quarterly) |@|
one[Digipack[Year.type]](plans, "Digipack year", FrontendId.Yearly)
) (DigipackPlans)
supporterPlus <- (
one[SupporterPlus[Month.type]](plans, "Supporter Plus month", FrontendId.Monthly) |@|
one[SupporterPlus[Year.type]](plans, "Supporter Plus year", FrontendId.Yearly)
) (SupporterPlusPlans)
tierThree <- (
one[TierThree[Month.type]](plans, "Supporter Plus & Guardian Weekly Domestic - Monthly", FrontendId.TierThreeMonthlyDomestic) |@|
one[TierThree[Year.type]](plans, "Supporter Plus & Guardian Weekly Domestic - Annual", FrontendId.TierThreeAnnualDomestic) |@|
one[TierThree[Month.type]](plans, "Supporter Plus & Guardian Weekly ROW - Monthly", FrontendId.TierThreeMonthlyROW) |@|
one[TierThree[Year.type]](plans, "Supporter Plus & Guardian Weekly ROW - Annual", FrontendId.TierThreeAnnualROW) |@|
one[TierThree[Month.type]](plans, "Supporter Plus, Guardian Weekly Domestic & Archive - Monthly", FrontendId.TierThreeMonthlyDomesticV2) |@|
one[TierThree[Year.type]](plans, "Supporter Plus, Guardian Weekly Domestic & Archive - Annual", FrontendId.TierThreeAnnualDomesticV2) |@|
one[TierThree[Month.type]](plans, "Supporter Plus, Guardian Weekly ROW & Archive - Monthly", FrontendId.TierThreeMonthlyROWV2) |@|
one[TierThree[Year.type]](plans, "Supporter Plus, Guardian Weekly ROW & Archive - Annual", FrontendId.TierThreeAnnualROWV2)
) (TierThreePlans)
contributor <- one[Contributor](plans, "Contributor month", FrontendId.Monthly)
voucher <- many[Voucher](plans, "Paper voucher")
digitalVoucher <- many[DigitalVoucher](plans, "Paper digital voucher")
delivery <- many[Delivery](plans, "Paper delivery")
nationalDelivery <- many[NationalDelivery](plans, "Paper - National Delivery")
weeklyZoneA <- (
one[WeeklyZoneA[SixWeeks.type]](plans, "Weekly Zone A Six weeks", FrontendId.SixWeeks) |@|
one[WeeklyZoneA[Quarter.type]](plans, "Weekly Zone A quarter", FrontendId.Quarterly) |@|
one[WeeklyZoneA[Year.type]](plans, "Weekly Zone A year", FrontendId.Yearly) |@|
one[WeeklyZoneA[OneYear.type]](plans, "Weekly Zone A one year", FrontendId.OneYear)
) (WeeklyZoneAPlans)
weeklyZoneB <- (
one[WeeklyZoneB[Quarter.type]](plans, "Weekly Zone B quarter", FrontendId.Quarterly) |@|
one[WeeklyZoneB[Year.type]](plans, "Weekly Zone B year", FrontendId.Yearly) |@|
one[WeeklyZoneB[OneYear.type]](plans, "Weekly Zone B one year", FrontendId.OneYear)
) (WeeklyZoneBPlans)
weeklyZoneC <- (
one[WeeklyZoneC[SixWeeks.type]](plans, "Weekly Zone C Six weeks", FrontendId.SixWeeks) |@|
one[WeeklyZoneC[Quarter.type]](plans, "Weekly Zone C quarter", FrontendId.Quarterly) |@|
one[WeeklyZoneC[Year.type]](plans, "Weekly Zone C year", FrontendId.Yearly)
) (WeeklyZoneCPlans)
weeklyDomestic <- (
one[WeeklyDomestic[SixWeeks.type]](plans, "Weekly Domestic Six weeks", FrontendId.SixWeeks) |@|
one[WeeklyDomestic[Quarter.type]](plans, "Weekly Domestic quarter", FrontendId.Quarterly) |@|
one[WeeklyDomestic[Year.type]](plans, "Weekly Domestic year", FrontendId.Yearly) |@|
one[WeeklyDomestic[Month.type]](plans, "Weekly Domestic month", FrontendId.Monthly) |@|
one[WeeklyDomestic[OneYear.type]](plans, "Weekly Domestic one year", FrontendId.OneYear) |@|
one[WeeklyDomestic[ThreeMonths.type]](plans, "Weekly Domestic three months", FrontendId.ThreeMonths)
) (WeeklyDomesticPlans)
weeklyRestOfWorld <- (
one[WeeklyRestOfWorld[SixWeeks.type]](plans, "Weekly Rest of World Six weeks", FrontendId.SixWeeks) |@|
one[WeeklyRestOfWorld[Quarter.type]](plans, "Weekly Rest of World quarter", FrontendId.Quarterly) |@|
one[WeeklyRestOfWorld[Year.type]](plans, "Weekly Rest of World year", FrontendId.Yearly) |@|
one[WeeklyRestOfWorld[Month.type]](plans, "Weekly Rest of World month", FrontendId.Monthly) |@|
one[WeeklyRestOfWorld[OneYear.type]](plans, "Weekly Rest of World one year", FrontendId.OneYear) |@|
one[WeeklyRestOfWorld[ThreeMonths.type]](plans, "Weekly Rest of World three months", FrontendId.ThreeMonths)
) (WeeklyRestOfWorldPlans)
weekly = WeeklyPlans(weeklyZoneA, weeklyZoneB, weeklyZoneC, weeklyDomestic, weeklyRestOfWorld)
map <- Validation.s[NonEmptyList[String]](plans.map(p => p.id -> p).toMap)
} yield Catalog(digipack, supporterPlus, tierThree, contributor, voucher, digitalVoucher, delivery, nationalDelivery, weekly, map)
lazy val catalog: M[NonEmptyList[String] \/ Catalog] = (for {
plans <- EitherT[String, M, List[CatalogZuoraPlan]](joinUp).leftMap(_.wrapNel)
catalog <- EitherT(validatePlans(plans).toDisjunction.point[M])
} yield catalog).run
lazy val unsafeCatalog: Catalog = unsafeGet(catalog.map(
_.valueOr(e => throw new IllegalStateException(s"$e while getting catalog"))
))
}