Skip to content

Commit

Permalink
leftovers
Browse files Browse the repository at this point in the history
  • Loading branch information
shtukas committed Sep 10, 2024
1 parent 516db46 commit 53382f2
Show file tree
Hide file tree
Showing 17 changed files with 37,029 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/coding-directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Coding Conventions

The price migration engine doesn't have coding conventions per se. ZIO does a very good job at keeping sanity between pure and impure code, and putting adhoc code into migration specific objects (the so called "modern" migrations) helps separate the general engine logic from specific requests. We also rely on the coding expertise of contributors to do the right thing (including breaking rules when needed).

With that said, we have the following conventions

- Coding Directive #1: When using `MigrationType(cohortSpec)` to dispatch values or behaviour per migration, and unless exceptions (there are a couple in the code for when we handle exceptions or for exceptional circumstances), we will be explicit on what we want and declaring all the cases. If somebody is implementing a new migration and follows the steps Pascal presented during GW2024, then declaring a new case will happen during the [first step](https://github.com/guardian/price-migration-engine/pull/1012). The reason for this rule is that an inexperienced contributor could easily miss a place in the code where some thought should have been given on what a new migration should specify. If the code compiles without prompting that decision the contributor might miss it. And even if the decision is to go with the "default", this needs to be explicitly specified. This convention was introduced in this pull request [pull:1022](https://github.com/guardian/price-migration-engine/pull/1022).

Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
package pricemigrationengine.migrations
import pricemigrationengine.model.ZuoraRatePlan
import pricemigrationengine.model._

import java.time.LocalDate

object SupporterPlus2024Migration {

// ------------------------------------------------
// Static Data
// ------------------------------------------------

val maxLeadTime = 33
val minLeadTime = 31

val pricesMonthlyOld: Map[String, Double] = Map(
"GBP" -> 10,
"USD" -> 13,
"CAD" -> 13,
"AUD" -> 17,
"NZD" -> 17,
"EUR" -> 10
)

val pricesAnnualOld: Map[String, Double] = Map(
"GBP" -> 95,
"USD" -> 120,
"CAD" -> 120,
"AUD" -> 160,
"NZD" -> 160,
"EUR" -> 95
)

val pricesMonthlyNew: Map[String, Double] = Map(
"GBP" -> 12,
"USD" -> 15,
"CAD" -> 15,
"AUD" -> 20,
"NZD" -> 20,
"EUR" -> 12
)

val pricesAnnualNew: Map[String, Double] = Map(
"GBP" -> 120,
"USD" -> 150,
"CAD" -> 150,
"AUD" -> 200,
"NZD" -> 200,
"EUR" -> 120
)

// ------------------------------------------------
// Data Functions
// ------------------------------------------------

// ------------------------------------------------
// Prices

def getOldPrice(billingPeriod: BillingPeriod, currency: String): Option[Double] = {
billingPeriod match {
case Monthly => pricesMonthlyOld.get(currency)
case Annual => pricesAnnualOld.get(currency)
case _ => None
}
}

def getNewPrice(billingPeriod: BillingPeriod, currency: String): Option[Double] = {
billingPeriod match {
case Monthly => pricesMonthlyNew.get(currency)
case Annual => pricesAnnualNew.get(currency)
case _ => None
}
}

// ------------------------------------------------
// Cancellation Saves

def cancellationSaveRatePlan(subscription: ZuoraSubscription): Option[ZuoraRatePlan] = {
subscription.ratePlans.find(rp => rp.ratePlanName.contains("Cancellation Save Discount"))
}

def isInCancellationSave(subscription: ZuoraSubscription): Boolean = {
cancellationSaveRatePlan(subscription: ZuoraSubscription).isDefined
}

def cancellationSaveEffectiveDate(subscription: ZuoraSubscription): Option[LocalDate] = {
for {
ratePlan <- cancellationSaveRatePlan(subscription)
charge <- ratePlan.ratePlanCharges.headOption
date <- charge.effectiveStartDate
} yield date
}

def isUnderActiveCancellationSave(subscription: ZuoraSubscription, today: LocalDate): Boolean = {
cancellationSaveEffectiveDate(subscription: ZuoraSubscription) match {
case None => false
case Some(date) => (date == today) || today.isBefore(date)
}
}

// ------------------------------------------------
// Subscription Data

def supporterPlusV2RatePlan(subscription: ZuoraSubscription): Either[AmendmentDataFailure, ZuoraRatePlan] = {
subscription.ratePlans.find(rp =>
rp.ratePlanName.contains("Supporter Plus V2") && !rp.lastChangeType.contains("Remove")
) match {
case None =>
Left(
AmendmentDataFailure(
s"Subscription ${subscription.subscriptionNumber} doesn't have any `Add`ed rate plan with pattern `Supporter Plus V2`"
)
)
case Some(ratePlan) => Right(ratePlan)
}
}

def supporterPlusBaseRatePlanCharge(
subscriptionNumber: String,
ratePlan: ZuoraRatePlan
): Either[AmendmentDataFailure, ZuoraRatePlanCharge] = {
ratePlan.ratePlanCharges.find(rpc => rpc.name.contains("Supporter Plus")) match {
case None => {
Left(
AmendmentDataFailure(s"Subscription ${subscriptionNumber} has a rate plan (${ratePlan}), but with no charge")
)
}
case Some(ratePlanCharge) => Right(ratePlanCharge)
}
}

def supporterPlusContributionRatePlanCharge(
subscriptionNumber: String,
ratePlan: ZuoraRatePlan
): Either[AmendmentDataFailure, ZuoraRatePlanCharge] = {
ratePlan.ratePlanCharges.find(rpc => rpc.name.contains("Contribution")) match {
case None => {
Left(
AmendmentDataFailure(s"Subscription ${subscriptionNumber} has a rate plan (${ratePlan}), but with no charge")
)
}
case Some(ratePlanCharge) => Right(ratePlanCharge)
}
}

// ------------------------------------------------
// Notification helpers

/*
Date: September 2024
Author: Pascal
Comment Group: 602514a6-5e53
These functions have been added to implement the extra fields added to EmailPayloadSubscriberAttributes
as part of the set up of the SupporterPlus 2024 migration (see Comment Group: 602514a6-5e53)
*/

def previousBaseAmount(subscription: ZuoraSubscription): Either[Failure, Option[BigDecimal]] = {
for {
ratePlan <- supporterPlusV2RatePlan(subscription)
ratePlanCharge <- supporterPlusBaseRatePlanCharge(subscription.subscriptionNumber, ratePlan)
} yield ratePlanCharge.price
}

def newBaseAmount(subscription: ZuoraSubscription): Either[Failure, Option[BigDecimal]] = {
for {
ratePlan <- supporterPlusV2RatePlan(subscription)
billingPeriod <- ZuoraRatePlan.ratePlanToBillingPeriod(ratePlan).toRight(AmendmentDataFailure(""))
ratePlanCharge <- supporterPlusBaseRatePlanCharge(subscription.subscriptionNumber, ratePlan)
currency = ratePlanCharge.currency
oldBaseAmountOpt <- previousBaseAmount(subscription)
oldBaseAmount <- oldBaseAmountOpt.toRight(
AmendmentDataFailure(
s"(error: 164d8f1c-6dc6) could not extract base amount for subscription ${subscription.subscriptionNumber}"
)
)
newPriceFromPriceGrid <- getNewPrice(billingPeriod, currency)
.map(BigDecimal(_))
.toRight(
AmendmentDataFailure(
s"(error: 611aedea-0478) could not getNewPrice for (billingPeriod, currency) and (${billingPeriod}, ${currency})"
)
)
} yield {
Some((oldBaseAmount * BigDecimal(1.27)).min(newPriceFromPriceGrid))
}
}

def contributionAmount(subscription: ZuoraSubscription): Either[Failure, Option[BigDecimal]] = {
for {
ratePlan <- supporterPlusV2RatePlan(subscription)
ratePlanCharge <- supporterPlusContributionRatePlanCharge(subscription.subscriptionNumber, ratePlan)
} yield ratePlanCharge.price
}

def previousCombinedAmount(subscription: ZuoraSubscription): Either[Failure, Option[BigDecimal]] = {
for {
contributionAmountOpt <- contributionAmount(subscription)
previousBaseAmountOpt <- previousBaseAmount(subscription)
} yield (
for {
contributionAmount <- contributionAmountOpt
previousBaseAmount <- previousBaseAmountOpt
} yield contributionAmount + previousBaseAmount
)
}

def newCombinedAmount(subscription: ZuoraSubscription): Either[Failure, Option[BigDecimal]] = {
for {
contributionAmountOpt <- contributionAmount(subscription)
previousBaseAmountOpt <- newBaseAmount(subscription)
} yield (
for {
contributionAmount <- contributionAmountOpt
previousBaseAmount <- previousBaseAmountOpt
} yield contributionAmount + previousBaseAmount
)
}

def hasNonTrivialContribution(subscription: ZuoraSubscription): Either[Failure, Boolean] = {
for {
amountOpt <- contributionAmount(subscription: ZuoraSubscription)
amount <- amountOpt.toRight(
AmendmentDataFailure(
s"(error: 232760f5) could not extract contribution amount for subscription ${subscription.subscriptionNumber}"
)
)
} yield amount > 0
}

// -------------------------------------------------------------------
// Braze names

def brazeName(subscription: ZuoraSubscription): Either[Failure, String] = {
for {
status <- hasNonTrivialContribution(subscription: ZuoraSubscription)
} yield {
if (status) {
"SV_SP2_Contributors_PriceRise2024"
} else {
"SV_SP2_PriceRise2024"
}
}
}

// ------------------------------------------------
// Primary Interface
// ------------------------------------------------

def priceData(
subscription: ZuoraSubscription
): Either[AmendmentDataFailure, PriceData] = {
for {
ratePlan <- supporterPlusV2RatePlan(subscription)
billingPeriod <- ZuoraRatePlan.ratePlanToBillingPeriod(ratePlan).toRight(AmendmentDataFailure(""))
ratePlanCharge <- supporterPlusBaseRatePlanCharge(subscription.subscriptionNumber, ratePlan)
currency = ratePlanCharge.currency
oldPrice <- ratePlanCharge.price.toRight(AmendmentDataFailure(""))
newPrice <- getNewPrice(billingPeriod, currency).toRight(AmendmentDataFailure(""))
} yield PriceData(currency, oldPrice, newPrice, BillingPeriod.toString(billingPeriod))
}

def zuoraUpdate(
subscription: ZuoraSubscription,
effectiveDate: LocalDate,
): Either[AmendmentDataFailure, ZuoraSubscriptionUpdate] = {
for {
existingRatePlan <- supporterPlusV2RatePlan(subscription)
existingBaseRatePlanCharge <- supporterPlusBaseRatePlanCharge(
subscription.subscriptionNumber,
existingRatePlan
)
billingPeriod <- ZuoraRatePlan
.ratePlanToBillingPeriod(existingRatePlan)
.toRight(
AmendmentDataFailure(
s"[17469705] Could not determine the billing period for subscription ${subscription.subscriptionNumber}"
)
)
} yield {
ZuoraSubscriptionUpdate(
add = List(
AddZuoraRatePlan(
productRatePlanId = existingRatePlan.productRatePlanId,
contractEffectiveDate = effectiveDate,
chargeOverrides = List(
ChargeOverride(
productRatePlanChargeId = existingBaseRatePlanCharge.productRatePlanChargeId,
billingPeriod = BillingPeriod.toString(billingPeriod),
price = 120.0
)
)
)
),
remove = List(
RemoveZuoraRatePlan(
ratePlanId = existingRatePlan.id,
contractEffectiveDate = effectiveDate
)
),
currentTerm = None,
currentTermPeriodType = None
)
}
}
}
15 changes: 15 additions & 0 deletions lambda/src/main/scala/pricemigrationengine/model/Estimation1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package pricemigrationengine.model

import java.time.LocalDate

object Estimation1 {
def isProcessable(item: CohortItem, today: LocalDate): Boolean = {
// This function looks at the `doNotProcessUntil` attribute and returns whether the item
// should go through the Estimation step. See comment group: 6157ec78
// Note that LocalDate.isAfter is strict (see EstimationTest for details)
item.doNotProcessUntil match {
case None => true
case Some(date) => (today == date) || today.isAfter(date)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"basicInfo" : {
"id" : "ACCOUNT-ID",
"name" : "ACCOUNT-NAME",
"accountNumber" : "ACCOUNT-NUMBER",
"notes" : null,
"status" : "Active",
"crmId" : "ACCOUNT-NAME",
"batch" : "Batch1",
"invoiceTemplateId" : "2c92a0fd5ecce80c015ee71028643020",
"communicationProfileId" : null,
"profileNumber" : null,
"purchaseOrderNumber" : null,
"customerServiceRepName" : null,
"ScrubbedOn__c" : null,
"IdentityId__c" : "IDENTITY-ID-C",
"sfContactId__c" : "SF-CONTACT-ID-C",
"CCURN__c" : null,
"SpecialDeliveryInstructions__c" : null,
"NonStandardDataReason__c" : null,
"Scrubbed__c" : null,
"ProcessingAdvice__c" : "NoAdvice",
"CreatedRequestId__c" : "cd0ca593-02bf-4f9a-0000-00000000bff6",
"RetryStatus__c" : null,
"salesRep" : null,
"creditMemoTemplateId" : null,
"debitMemoTemplateId" : null,
"summaryStatementTemplateId" : null,
"sequenceSetId" : null
},
"billingAndPayment" : null,
"metrics" : {
"balance" : 0.000000000,
"currency" : "AUD",
"totalInvoiceBalance" : 0.000000000,
"creditBalance" : 0.000000000,
"totalDebitMemoBalance" : 0.000000000,
"unappliedPaymentAmount" : 0.000000000,
"unappliedCreditMemoAmount" : 0.000000000,
"contractedMrr" : 41.670000000
},
"billToContact" : null,
"soldToContact" : null,
"taxInfo" : null,
"success" : true
}
Loading

0 comments on commit 53382f2

Please sign in to comment.