Skip to content

Commit

Permalink
better naming for subscription and catalog model
Browse files Browse the repository at this point in the history
  • Loading branch information
johnduffell committed May 29, 2024
1 parent c2233c4 commit 2eee502
Show file tree
Hide file tree
Showing 14 changed files with 81 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,15 @@ class AccountController(
case period: BillingPeriod.OneOffPeriod => Left(s"period $period was not recurring for contribution update")
})
applyFromDate = contributionPlan.chargedThroughDate.getOrElse(contributionPlan.start)
currency = contributionPlan.charges.head.pricing.prices.head.currency
currency = contributionPlan.ratePlanCharges.head.pricing.prices.head.currency
currencyGlyph = currency.glyph
oldPrice = contributionPlan.totalChargesMinorUnit.toDouble / 100
reasonForChange =
s"User updated contribution via self-service MMA. Amount changed from $currencyGlyph$oldPrice to $currencyGlyph$newPrice effective from $applyFromDate"
result <- SimpleEitherT(
services.zuoraRestService.updateChargeAmount(
subscription.name,
contributionPlan.charges.head.id,
contributionPlan.ratePlanCharges.head.id,
contributionPlan.id,
newPrice.toDouble,
reasonForChange,
Expand Down
14 changes: 7 additions & 7 deletions membership-attribute-service/app/models/AccountDetails.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package models
import com.gu.i18n.Country
import com.gu.memsub.Benefit.PaperDay
import com.gu.memsub.ProductRatePlanChargeProductType.PaperDay
import com.gu.memsub._
import com.gu.memsub.subsv2._
import com.gu.monitoring.SafeLogger.LogPrefix
Expand Down Expand Up @@ -97,26 +97,26 @@ object AccountDetails {
case _ => Json.obj()
}

def externalisePlanName(plan: SubscriptionZuoraPlan): Option[String] = plan.product(catalog) match {
def externalisePlanName(plan: RatePlan): Option[String] = plan.product(catalog) match {
case _: Product.Weekly => if (plan.name(catalog).contains("Six for Six")) Some("currently on '6 for 6'") else None
case _: Product.Paper => Some(plan.name(catalog).replace("+", " plus Digital Subscription"))
case _ => None
}

def maybePaperDaysOfWeek(plan: SubscriptionZuoraPlan) = {
def maybePaperDaysOfWeek(plan: RatePlan) = {
val dayNames = for {
charge <- plan.charges.list.toList
charge <- plan.ratePlanCharges.list.toList
.filterNot(_.pricing.isFree) // note 'Echo Legacy' rate plan has all days of week but some are zero price, this filters those out
catalogZuoraPlan <- catalog.catalogMap.get(plan.productRatePlanId)
dayName <- catalogZuoraPlan.benefits
dayName <- catalogZuoraPlan.productRatePlanCharges
.get(charge.productRatePlanChargeId)
.collect { case benefit: PaperDay => DayOfWeek.of(benefit.dayOfTheWeekIndex).getDisplayName(TextStyle.FULL, Locale.ENGLISH) }
} yield dayName

if (dayNames.nonEmpty) Json.obj("daysOfWeek" -> dayNames) else Json.obj()
}

def jsonifyPlan(plan: SubscriptionZuoraPlan) = Json.obj(
def jsonifyPlan(plan: RatePlan) = Json.obj(
"name" -> externalisePlanName(plan),
"start" -> plan.start,
"end" -> plan.end,
Expand All @@ -130,7 +130,7 @@ object AccountDetails {
"features" -> plan.features.map(_.featureCode).mkString(","),
) ++ maybePaperDaysOfWeek(plan)

val sortedPlans = subscription.lowLevelPlans.sortBy(_.start.toDate)
val sortedPlans = subscription.ratePlans.sortBy(_.start.toDate)
val currentPlans = sortedPlans.filter(plan => !plan.start.isAfter(now) && plan.end.isAfter(now))
val futurePlans = sortedPlans.filter(plan => plan.start.isAfter(now))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package models

import _root_.services.zuora.rest.ZuoraRestService.ObjectAccount
import com.gu.memsub._
import com.gu.memsub.subsv2.{Catalog, Subscription, SubscriptionZuoraPlan}
import com.gu.memsub.subsv2.{Catalog, Subscription, RatePlan}
import com.gu.monitoring.SafeLogging
import org.joda.time.LocalDate.now
import play.api.libs.json.{JsObject, Json}
Expand All @@ -20,7 +20,7 @@ object ExistingPaymentOption {

import existingPaymentOption._

private def getSubscriptionFriendlyName(plan: SubscriptionZuoraPlan, catalog: Catalog): String = plan.product(catalog) match {
private def getSubscriptionFriendlyName(plan: RatePlan, catalog: Catalog): String = plan.product(catalog) match {
case _: Product.Weekly => "Guardian Weekly"
case _: Product.Membership => plan.productName + " Membership"
case _: Product.Contribution => plan.name(catalog)
Expand All @@ -47,7 +47,7 @@ object ExistingPaymentOption {
"billingAccountId" -> subscription.accountId.get, // this could be different to the top level one due to consolidation
"isCancelled" -> subscription.isCancelled,
"isActive" -> (!subscription.isCancelled && !subscription.termEndDate.isBefore(now)),
"name" -> subscription.lowLevelPlans.headOption.map { plan =>
"name" -> subscription.ratePlans.headOption.map { plan =>
getSubscriptionFriendlyName(plan, catalog)
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ class GuardianPatronService(
casActivationDate = None,
promoCode = None,
isCancelled = subscription.isCancelled,
lowLevelPlans = List(
SubscriptionZuoraPlan(
ratePlans = List(
RatePlan(
id = RatePlanId("guardian_patron_unused"), // only used for contribution amount change
productRatePlanId = Catalog.guardianPatronProductRatePlanId,
productName = "Guardian Patron",
lastChangeType = None,
features = Nil,
charges = NonEmptyList(
ZuoraCharge(
ratePlanCharges = NonEmptyList(
RatePlanCharge(
SubscriptionRatePlanChargeId(""), // only used for update contribution amount
ProductRatePlanChargeId(""), // benefit is only used for paper days (was Benefit.GuardianPatron)
PricingSummary(Map(subscription.plan.currency -> price)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ object PaymentFailureAlerter extends SafeLogging {
}

def getProductDescription(subscription: Subscription) =
if (subscription.lowLevelPlans.head.product(catalog) == Membership) {
if (subscription.ratePlans.head.product(catalog) == Membership) {
s"${subscription.plan(catalog).productName} membership"
} else if (subscription.lowLevelPlans.head.product(catalog) == Contribution) {
} else if (subscription.ratePlans.head.product(catalog) == Contribution) {
"contribution"
} else {
subscription.plan(catalog).productName
Expand Down
22 changes: 11 additions & 11 deletions membership-common/src/main/scala/com/gu/memsub/ProductFamily.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ object Product {
}

// Benefit is the catalog charge level ProductType__c
sealed trait Benefit {
sealed trait ProductRatePlanChargeProductType {
val id: String
override def toString = s"benefit $id"
}

object Benefit {
object ProductRatePlanChargeProductType {

def fromId(id: String): Option[Benefit] =
def fromId(id: String): Option[ProductRatePlanChargeProductType] =
PaperDay.fromId(id) orElse
PaidMemberTier.fromId(id) orElse
(id == SupporterPlus.id).option(SupporterPlus) orElse
Expand All @@ -109,9 +109,9 @@ object Benefit {
(id == Contributor.id).option(Contributor) orElse
(id == Weekly.id).option(Weekly)

sealed trait MemberTier extends Benefit
sealed trait MemberTier extends ProductRatePlanChargeProductType
sealed trait PaidMemberTier extends MemberTier
sealed trait PaperDay extends Benefit {
sealed trait PaperDay extends ProductRatePlanChargeProductType {
val dayOfTheWeekIndex: Int
}

Expand All @@ -137,7 +137,7 @@ object Benefit {
}
}

object Contributor extends Benefit {
object Contributor extends ProductRatePlanChargeProductType {
override val id = "Contributor"
}

Expand All @@ -154,19 +154,19 @@ object Benefit {
}

// This is the new non-membership version of a patron
object GuardianPatron extends Benefit {
object GuardianPatron extends ProductRatePlanChargeProductType {
override val id = "Guardian Patron"
}

object Digipack extends Benefit {
object Digipack extends ProductRatePlanChargeProductType {
override val id = "Digital Pack"
}

object SupporterPlus extends Benefit {
object SupporterPlus extends ProductRatePlanChargeProductType {
override val id = "Supporter Plus"
}

object Weekly extends Benefit {
object Weekly extends ProductRatePlanChargeProductType {
override val id = "Guardian Weekly"
}

Expand Down Expand Up @@ -205,7 +205,7 @@ object Benefit {
override val dayOfTheWeekIndex = 7
}

object Adjustment extends Benefit {
object Adjustment extends ProductRatePlanChargeProductType {
override val id = "Adjustment"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.gu.memsub.subsv2.Catalog.CatalogMap

object Catalog {

type CatalogMap = Map[ProductRatePlanId, CatalogZuoraPlan]
type CatalogMap = Map[ProductRatePlanId, ProductRatePlan]

// dummy ids for stripe (non zuora) products
val guardianPatronProductRatePlanId: ProductRatePlanId = ProductRatePlanId("guardian_patron")
Expand Down
16 changes: 8 additions & 8 deletions membership-common/src/main/scala/com/gu/memsub/subsv2/Plan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object UpToPeriodsType {

/** Low level model of a Zuora rate plan charge
*/
case class ZuoraCharge(
case class RatePlanCharge(
id: SubscriptionRatePlanChargeId,
productRatePlanChargeId: ProductRatePlanChargeId,
pricing: PricingSummary,
Expand Down Expand Up @@ -162,23 +162,23 @@ case class ZuoraCharge(

/** Low level model of a rate plan, as it appears on a subscription in Zuora
*/
case class SubscriptionZuoraPlan(
case class RatePlan(
id: RatePlanId,
productRatePlanId: ProductRatePlanId,
productName: String,
lastChangeType: Option[String],
features: List[Feature],
chargedThroughDate: Option[LocalDate],
charges: NonEmptyList[ZuoraCharge],
ratePlanCharges: NonEmptyList[RatePlanCharge],
start: LocalDate,
end: LocalDate,
) {

def totalChargesMinorUnit: Int =
charges.map(c => (c.pricing.prices.head.amount * 100).toInt).list.toList.sum
ratePlanCharges.map(c => (c.pricing.prices.head.amount * 100).toInt).list.toList.sum

def chargesPrice: PricingSummary =
charges
ratePlanCharges
.map(_.pricing)
.list
.toList
Expand All @@ -189,7 +189,7 @@ case class SubscriptionZuoraPlan(
)

def billingPeriod: Validation[String, BillingPeriod] = {
val billingPeriods = charges.list.toList.map(c => c.billingPeriod).distinct
val billingPeriods = ratePlanCharges.list.toList.map(c => c.billingPeriod).distinct
billingPeriods match {
case Nil => Validation.f[BillingPeriod]("No billing period found")
case b :: Nil => b
Expand All @@ -214,11 +214,11 @@ case class SubscriptionZuoraPlan(

/** Low level model of a product rate plan, as it appears in the Zuora product catalog
*/
case class CatalogZuoraPlan(
case class ProductRatePlan(
id: ProductRatePlanId,
name: String,
productId: ProductId,
benefits: Map[ProductRatePlanChargeId, Benefit],
productRatePlanCharges: Map[ProductRatePlanChargeId, ProductRatePlanChargeProductType],
private val productTypeOption: Option[ProductType],
) {
lazy val productType: ProductType = productTypeOption.getOrElse(throw new RuntimeException("Product type is undefined for plan: " + name))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ case class Subscription(
casActivationDate: Option[DateTime],
promoCode: Option[PromoCode],
isCancelled: Boolean,
lowLevelPlans: List[SubscriptionZuoraPlan],
ratePlans: List[RatePlan],
readerType: ReaderType,
gifteeIdentityId: Option[String],
autoRenew: Boolean,
) {

val firstPaymentDate: LocalDate = (acceptanceDate :: lowLevelPlans.map(_.start)).min
val firstPaymentDate: LocalDate = (acceptanceDate :: ratePlans.map(_.start)).min

def plan(catalog: Catalog): SubscriptionZuoraPlan =
def plan(catalog: Catalog): RatePlan =
GetCurrentPlans.currentPlans(this, LocalDate.now, catalog).fold(error => throw new RuntimeException(error), _.head)

}
Expand All @@ -45,16 +45,16 @@ with the newest plan for upgrade and cancel scenarios, so in this case the most
*/
object GetCurrentPlans {

def bestCancelledPlan(sub: Subscription): Option[SubscriptionZuoraPlan] =
def bestCancelledPlan(sub: Subscription): Option[RatePlan] =
if (sub.isCancelled && sub.termEndDate.isBefore(LocalDate.now()))
sub.lowLevelPlans.sortBy(_.totalChargesMinorUnit).reverse.headOption
sub.ratePlans.sortBy(_.totalChargesMinorUnit).reverse.headOption
else None

case class DiscardedPlan(plan: SubscriptionZuoraPlan, why: String)
case class DiscardedPlan(plan: RatePlan, why: String)

def currentPlans(sub: Subscription, date: LocalDate, catalog: Catalog): String \/ NonEmptyList[SubscriptionZuoraPlan] = {
def currentPlans(sub: Subscription, date: LocalDate, catalog: Catalog): String \/ NonEmptyList[RatePlan] = {

val currentPlans = sub.lowLevelPlans.sortBy(_.totalChargesMinorUnit).reverse.map { plan =>
val currentPlans = sub.ratePlans.sortBy(_.totalChargesMinorUnit).reverse.map { plan =>
val product = plan.product(catalog)
// If the sub hasn't been paid yet but has started we should fast-forward to the date of first payment (free trial)
val dateToCheck = if (sub.startDate <= date && sub.acceptanceDate > date) sub.acceptanceDate else date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,54 @@ import scalaz.syntax.traverse._

object CatJsonReads {

private implicit val ProductReads: Reads[Benefit] = new Reads[Benefit] {
override def reads(json: JsValue): JsResult[Benefit] = json match {
case JsString(id) => Benefit.fromId(id).fold[JsResult[Benefit]](JsError(s"Bad product $id"))(e => JsSuccess(e))
private implicit val productRatePlanChargeProductTypeReads: Reads[ProductRatePlanChargeProductType] = new Reads[ProductRatePlanChargeProductType] {
override def reads(json: JsValue): JsResult[ProductRatePlanChargeProductType] = json match {
case JsString(id) =>
ProductRatePlanChargeProductType.fromId(id).fold[JsResult[ProductRatePlanChargeProductType]](JsError(s"Bad product $id"))(e => JsSuccess(e))
case a => JsError(s"Malformed product JSON, needed a string but got $a")
}
}

private implicit val catalogZuoraPlanBenefitReads: Reads[(ProductRatePlanChargeId, Benefit)] = (
private implicit val productRatePlanChargeReads: Reads[(ProductRatePlanChargeId, ProductRatePlanChargeProductType)] = (
(__ \ "id").read[String].map(ProductRatePlanChargeId) and
(__ \ "ProductType__c").read[Benefit]
(__ \ "ProductType__c").read[ProductRatePlanChargeProductType]
)(_ -> _)

private val listOfProductsReads = new Reads[Map[ProductRatePlanChargeId, Benefit]] {
override def reads(json: JsValue): JsResult[Map[ProductRatePlanChargeId, Benefit]] = json match {
private val productRatePlanChargesReads = new Reads[Map[ProductRatePlanChargeId, ProductRatePlanChargeProductType]] {
override def reads(json: JsValue): JsResult[Map[ProductRatePlanChargeId, ProductRatePlanChargeProductType]] = json match {
case JsArray(vals) =>
vals
.map(_.validate[(ProductRatePlanChargeId, Benefit)])
.map(_.validate[(ProductRatePlanChargeId, ProductRatePlanChargeProductType)])
.filter(_.isSuccess)
.toList // bad things are happening here, we're chucking away errors
.sequence[JsResult, (ProductRatePlanChargeId, Benefit)]
.sequence[JsResult, (ProductRatePlanChargeId, ProductRatePlanChargeProductType)]
.map(_.toMap)
case _ => JsError("No valid benefits found")
}
}

private def catalogZuoraPlanReads(productType: Option[ProductType], pid: ProductId): Reads[CatalogZuoraPlan] =
private def productRatePlanReads(productType: Option[ProductType], pid: ProductId): Reads[ProductRatePlan] =
(json: JsValue) => {
((__ \ "id").read[String].map(ProductRatePlanId) and
(__ \ "name").read[String] and
Reads.pure(pid) and
(__ \ "productRatePlanCharges").read[Map[ProductRatePlanChargeId, Benefit]](listOfProductsReads) and
Reads.pure(productType))(CatalogZuoraPlan.apply _).reads(json)
(__ \ "productRatePlanCharges").read[Map[ProductRatePlanChargeId, ProductRatePlanChargeProductType]](productRatePlanChargesReads) and
Reads.pure(productType))(ProductRatePlan.apply _).reads(json)
}

val catalogZuoraPlanListReads: Reads[List[CatalogZuoraPlan]] =
val productsReads: Reads[List[ProductRatePlan]] =
(json: JsValue) =>
json \ "products" match {
case JsDefined(JsArray(products)) =>
products.toList
.map { product =>
val productId = (product \ "id").as[String]
val productType = (product \ "ProductType__c").asOpt[String].map(ProductType)
val reads = catalogZuoraPlanReads(productType, ProductId(productId))
(product \ "productRatePlans").validate[List[CatalogZuoraPlan]](niceListReads(reads))
val reads = productRatePlanReads(productType, ProductId(productId))
(product \ "productRatePlans").validate[List[ProductRatePlan]](niceListReads(reads))
}
.filter(_.isSuccess)
.sequence[JsResult, List[CatalogZuoraPlan]]
.sequence[JsResult, List[ProductRatePlan]]
.map(_.flatten)
case a => JsError(s"No product array found, got $a")
}
Expand Down
Loading

0 comments on commit 2eee502

Please sign in to comment.