app/com/gu/memsub/subsv2/reads/ChargeListReads.scala (202 lines of code) (raw):
package com.gu.memsub.subsv2.reads
import com.gu.memsub.Benefit._
import com.gu.memsub.BillingPeriod._
import com.gu.memsub.Subscription.{ProductId, ProductRatePlanChargeId}
import com.gu.memsub._
import com.gu.memsub.subsv2._
import com.gu.memsub.subsv2.reads.ChargeListReads.PlanChargeMap
import com.gu.memsub.subsv2.reads.CommonReads.{FailureAggregatingOrElse, TraceableValidation}
import scala.reflect.ClassTag
import scalaz._
import scalaz.syntax.applicative._
import scalaz.syntax.nel._
import scalaz.syntax.std.boolean._
import scalaz.syntax.std.option._
import scalaz.Validation.FlatMap._
/**
* Try to convert a single ZuoraCharge into some type A
*/
trait ChargeReads[A] {
def read(cat: PlanChargeMap, charge: ZuoraCharge): ValidationNel[String, A]
// only read if the result is the given type
def filter[B <: A : ClassTag]: ChargeReads[B] = {
val requiredClass = implicitly[ClassTag[B]].runtimeClass
new ChargeReads[B] {
override def read(cat: PlanChargeMap, charge: ZuoraCharge): ValidationNel[String, B] =
ChargeReads.this.read(cat, charge).flatMap {
case b: B if requiredClass.isInstance(b) => Success(b)
case actual => Failure(s"expected $requiredClass but was $actual").toValidationNel
}
}
}
}
/**
* Try to convert a list of Zuora charges into some type A
*/
trait ChargeListReads[A] {
def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, A]
}
object ChargeListReads {
type PlanChargeMap = Map[ProductRatePlanChargeId, Benefit]
def pure[A](a: A) = new ChargeListReads[A] {
override def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, A] =
Validation.s[NonEmptyList[String]](a)
}
case class ProductIds(
weeklyZoneA: ProductId,
weeklyZoneB: ProductId,
weeklyZoneC: ProductId,
weeklyDomestic: ProductId,
weeklyRestOfWorld: ProductId,
digipack: ProductId,
supporterPlus: ProductId,
tierThree: ProductId,
voucher: ProductId,
digitalVoucher: ProductId,
delivery: ProductId,
nationalDelivery: ProductId,
contributor: ProductId
)
// shorthand syntax for creating new ChargeListReads
def apply[A](f: (PlanChargeMap, List[ZuoraCharge]) => ValidationNel[String, A]) = new ChargeListReads[A] {
override def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, A] = f(cat, charges)
}
implicit def readAnyProduct[P <: Benefit : ClassTag]: ChargeReads[P] = new ChargeReads[Benefit] {
def read(cat: PlanChargeMap, charge: ZuoraCharge): ValidationNel[String, Benefit] =
cat.get(charge.productRatePlanChargeId).toSuccess(NonEmptyList(s"Could not find product ${charge.name} ${charge.productRatePlanChargeId} in catalog"))
}.filter[P]
implicit def anyBpReads[B <: BillingPeriod : ClassTag]: ChargeReads[B] = new ChargeReads[BillingPeriod] {
def read(cat: PlanChargeMap, charge: ZuoraCharge): ValidationNel[String, BillingPeriod] = {
((charge.endDateCondition, charge.billingPeriod) match {
case (FixedPeriod, Some(ZSpecificWeeks))
if charge.specificBillingPeriod.exists(numberOfWeeks => numberOfWeeks == 6 || numberOfWeeks == 7) &&
charge.upToPeriods.contains(1) &&
charge.upToPeriodsType.contains(BillingPeriods) =>
Validation.success[String, BillingPeriod](SixWeeks)
case (FixedPeriod, Some(zPeriod))
if charge.upToPeriods.contains(1) &&
charge.upToPeriodsType.contains(BillingPeriods) =>
zPeriod match {
case ZYear => Validation.success[String, BillingPeriod](OneYear)
case ZQuarter => Validation.success[String, BillingPeriod](ThreeMonths)
case ZTwoYears => Validation.success[String, BillingPeriod](TwoYears)
case ZThreeYears => Validation.success[String, BillingPeriod](ThreeYears)
case ZSemiAnnual => Validation.success[String, BillingPeriod](SixMonths)
case _ => Validation.f[BillingPeriod](s"zuora fixed period was $zPeriod")
}
case (SubscriptionEnd, Some(zPeriod)) =>
zPeriod match {
case ZMonth => Validation.success[String, BillingPeriod](Month)
case ZQuarter => Validation.success[String, BillingPeriod](Quarter)
case ZYear => Validation.success[String, BillingPeriod](Year)
case ZSemiAnnual => Validation.success[String, BillingPeriod](SixMonthsRecurring)
case _ => Validation.f[BillingPeriod](s"zuora recurring period was $zPeriod")
}
case (OneTime, None) => Validation.success[String, BillingPeriod](OneTimeChargeBillingPeriod) // This represents a one time rate plan charge
case _ =>
Validation.f[BillingPeriod](s"period =${charge.billingPeriod} specificBillingPeriod=${charge.specificBillingPeriod} uptoPeriodsType=${charge.upToPeriodsType}, uptoPeriods=${charge.upToPeriods}")
}).toValidationNel.withTrace("anyBpReads")
}
}.filter[B]
implicit def readPaidCharge[P <: Benefit, BP <: BillingPeriod](implicit product: ChargeReads[P], bp: ChargeReads[BP]): ChargeListReads[PaidCharge[P, BP]] = new ChargeListReads[PaidCharge[P, BP]] {
def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, PaidCharge[P, BP]] = charges match {
case charge :: Nil => (product.read(cat, charge) |@| bp.read(cat, charge) |@|
charge.pricing.prices.exists(_.amount != 0).option(charge.pricing).toSuccess(NonEmptyList("Could not read paid charge: Charge is free")))
.apply({ case(p, b, pricing) => PaidCharge(p, b, pricing, charge.productRatePlanChargeId, charge.id) })
case charge :: others => Validation.failureNel(s"Too many charges! I got $charge and $others")
case Nil => Validation.failureNel(s"No charges found!")
}
}
implicit def readFreeCharge[P <: Benefit](implicit product: ChargeReads[P]): ChargeListReads[FreeCharge[P]] = new ChargeListReads[FreeCharge[P]] {
def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, FreeCharge[P]] = charges match {
case charge :: Nil => (product.read(cat, charge) |@| charge.pricing.prices.forall(_.amount == 0).option(charge.pricing)
.toSuccess(NonEmptyList("Could not read free charge: Charge is paid"))).apply({ case (p, _) => FreeCharge(p, charge.pricing.currencies) })
case charge :: others => Validation.failureNel(s"Too many charges! I got $charge and $others")
case Nil => Validation.failureNel(s"No charges found!")
}
}
implicit def readPaidChargeList: ChargeListReads[PaidChargeList] = new ChargeListReads[PaidChargeList] {
def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, PaidChargeList] = {
readPaperChargeList.read(cat, charges).map(identity[PaidChargeList]) orElse2
readPaidCharge[Benefit, BillingPeriod](readAnyProduct, anyBpReads).read(cat, charges) orElse2
readSupporterPlusV2ChargeList.read(cat, charges) orElse2
readTierThreeChargeList.read(cat, charges)
}.withTrace("readPaidChargeList")
}
implicit def readFreeChargeList: ChargeListReads[FreeChargeList] = new ChargeListReads[FreeChargeList] {
def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, FreeChargeList] = {
readFreeCharge[Benefit](readAnyProduct).read(cat, charges).map(identity[FreeChargeList])
}
}
implicit def readChargeList: ChargeListReads[ChargeList] = new ChargeListReads[ChargeList] {
override def read(cat: PlanChargeMap, charges: List[ZuoraCharge]) = {
readPaidChargeList.read(cat, charges) orElse2
readFreeChargeList.read(cat, charges)
}.withTrace("readChargeList")
}
implicit def readPaperChargeList: ChargeListReads[PaperCharges] = new ChargeListReads[PaperCharges] {
def findDigipack(chargeMap: List[(Benefit, PricingSummary)]): ValidationNel[String, Option[PricingSummary]] =
chargeMap.collect { case (Digipack, p) => (Digipack, p) } match {
case Nil => Validation.success[NonEmptyList[String], Option[PricingSummary]](None)
case n :: Nil => Validation.s[NonEmptyList[String]](Some(n._2))
case n :: ns => Validation.failureNel("Too many digipacks")
}
def getDays(chargeMap: List[(Benefit, PricingSummary)]): ValidationNel[String, Map[PaperDay, PricingSummary]] = {
val foundDays = chargeMap.collect({ case(d: PaperDay, p) => (d, p) })
Validation.success(foundDays.toMap)
.ensure("There are duplicate days".wrapNel)(_.size == foundDays.size)
.ensure("No days found".wrapNel)(_.nonEmpty)
}
override def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, PaperCharges] = {
val chargeMap = charges.flatMap(c => cat.get(c.productRatePlanChargeId).map(_ -> c.pricing))
(getDays(chargeMap) |@| findDigipack(chargeMap))(PaperCharges).withTrace("readPaperChargeList")
}
}
implicit def readSupporterPlusV2ChargeList: ChargeListReads[SupporterPlusCharges] = new ChargeListReads[SupporterPlusCharges] {
def validateBillingPeriod(zBillingPeriod: ZBillingPeriod): ValidationNel[String, BillingPeriod] = zBillingPeriod match {
case ZMonth => Validation.success[NonEmptyList[String], BillingPeriod](Month)
case ZYear => Validation.success[NonEmptyList[String], BillingPeriod](Year)
case _ => Validation.failureNel(s"Supporter plus V2 must have a Monthly or Annual billing period, not $zBillingPeriod")
}
def getBillingPeriod(charges: List[ZuoraCharge]): ValidationNel[String, BillingPeriod] = {
val billingPeriods = charges.flatMap(_.billingPeriod).distinct
billingPeriods match {
case Nil => Validation.failureNel("No billing period found")
case b :: Nil => validateBillingPeriod(b)
case _ => Validation.failureNel("Too many billing periods found")
}
}
def getPricingSummaries(charges: List[ZuoraCharge]): ValidationNel[String, List[PricingSummary]] = {
val pricingSummaries = charges.map(_.pricing)
Validation
.success[NonEmptyList[String], List[PricingSummary]](pricingSummaries)
.ensure("No pricing summaries found".wrapNel)(_.nonEmpty)
}
override def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, SupporterPlusCharges] = {
(getBillingPeriod(charges) |@| getPricingSummaries(charges)).apply(SupporterPlusCharges)
}
}
implicit def readTierThreeChargeList: ChargeListReads[TierThreeCharges] = new ChargeListReads[TierThreeCharges] {
def validateBillingPeriod(zBillingPeriod: ZBillingPeriod): ValidationNel[String, BillingPeriod] = zBillingPeriod match {
case ZMonth => Validation.success[NonEmptyList[String], BillingPeriod](Month)
case ZYear => Validation.success[NonEmptyList[String], BillingPeriod](Year)
case _ => Validation.failureNel(s"Tier three must have a Monthly or Annual billing period, not $zBillingPeriod")
}
def getBillingPeriod(charges: List[ZuoraCharge]): ValidationNel[String, BillingPeriod] = {
val billingPeriods = charges.flatMap(_.billingPeriod).distinct
billingPeriods match {
case Nil => Validation.failureNel("No billing period found")
case b :: Nil => validateBillingPeriod(b)
case _ => Validation.failureNel("Too many billing periods found")
}
}
def getPricingSummaries(charges: List[ZuoraCharge]): ValidationNel[String, List[PricingSummary]] = {
val pricingSummaries = charges.map(_.pricing)
Validation
.success[NonEmptyList[String], List[PricingSummary]](pricingSummaries)
.ensure("No pricing summaries found".wrapNel)(_.nonEmpty)
}
override def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, TierThreeCharges] = {
(getBillingPeriod(charges) |@| getPricingSummaries(charges)).apply(TierThreeCharges)
}
}
implicit def readSingle[B <: Benefit : ChargeReads]: ChargeListReads[ChargeList with SingleBenefit[B]] =
new ChargeListReads[ChargeList with SingleBenefit[B]] {
def read(cat: PlanChargeMap, charges: List[ZuoraCharge]): ValidationNel[String, ChargeList with SingleBenefit[B]] = {
readPaidCharge[B, BillingPeriod].read(cat, charges).map(identity[ChargeList with SingleBenefit[B]]) orElse2
readFreeCharge[B].read(cat, charges)
}.withTrace("readSingle")
}
}