Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Newspaper2024 #964

Merged
merged 82 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
68cd1b4
implement prices
shtukas Dec 16, 2023
9c439dc
implement priceData
shtukas Dec 19, 2023
4c1bba7
Merge branch 'main' into ph-20231216-1541-newspaper-migration
shtukas Dec 19, 2023
8bbaa83
Introduce new MigrationType
shtukas Dec 19, 2023
0a2de5a
Prevent salesforce notification in the case of Newspaper2024
shtukas Dec 19, 2023
8335089
call the right price data
shtukas Dec 20, 2023
bd9b5fb
temporary special handle of estimation errors
shtukas Dec 20, 2023
a1bf0d5
add test
shtukas Dec 30, 2023
b1e4a70
add test
shtukas Dec 30, 2023
a46281e
add test
shtukas Dec 30, 2023
7bbf7b7
add test
shtukas Dec 30, 2023
eb16f02
add test
shtukas Dec 30, 2023
b9107ce
add test
shtukas Dec 30, 2023
ab6f3ea
add test
shtukas Dec 31, 2023
c640d0a
add test
shtukas Dec 31, 2023
5aaa4d2
add test
shtukas Dec 31, 2023
5e54369
add test
shtukas Dec 31, 2023
6b630c8
add test
shtukas Dec 31, 2023
e5d3feb
add test
shtukas Dec 31, 2023
f198d9c
add tests
shtukas Dec 31, 2023
320933c
test updates
shtukas Jan 1, 2024
886d29f
compute Newspaper2024BatchId
shtukas Jan 1, 2024
37b90c6
adjust starting date for non monthlies
shtukas Jan 2, 2024
c0d1904
Newspaper2024 startDateSpreadPeriod
shtukas Jan 2, 2024
36c53fc
Merge branch 'main' into ph-20231216-1541-newspaper-migration
shtukas Jan 2, 2024
897ae93
Allow all Newspaper2024 steps up to (but not including) notification
shtukas Jan 2, 2024
3da49b6
templates and first test for price distributions
shtukas Jan 8, 2024
47f4549
more price distributions
shtukas Jan 8, 2024
99840d6
pricing update (PriceDistributions)
shtukas Jan 9, 2024
6de79a6
Merge branch 'main' into ph-20231216-1541-newspaper-migration
shtukas Jan 9, 2024
8b41f3a
update test fixture
shtukas Jan 9, 2024
dbd6783
update test fixture
shtukas Jan 9, 2024
efca2d5
update test fixture
shtukas Jan 9, 2024
259537e
update test fixture
shtukas Jan 9, 2024
261fc2a
update test fixture
shtukas Jan 9, 2024
6f50711
update test fixture
shtukas Jan 9, 2024
14d0c18
update test fixture
shtukas Jan 9, 2024
07a188f
update test fixture
shtukas Jan 9, 2024
cb18096
Merge branch 'main' into ph-20231216-1541-newspaper-migration
shtukas Jan 9, 2024
74f8f49
upgrade tests
shtukas Jan 10, 2024
2a6a01a
test update
shtukas Jan 11, 2024
4215ac8
test update
shtukas Jan 11, 2024
0da6645
towards amendment objects
shtukas Jan 11, 2024
59488a7
perform one newspaper notification
shtukas Jan 12, 2024
424fd72
allow newspaper notifications
shtukas Jan 12, 2024
e60144f
support for missing country
shtukas Jan 12, 2024
dd48f9f
refactoring
shtukas Jan 13, 2024
d929cc9
refactoring
shtukas Jan 13, 2024
34f4e53
refactoring
shtukas Jan 13, 2024
694efe6
refactoring
shtukas Jan 13, 2024
4c0902b
refactoring
shtukas Jan 13, 2024
1a6b74f
Towards ZuoraSubscriptionUpdate (1)
shtukas Jan 13, 2024
6bf93af
Towards ZuoraSubscriptionUpdate (2)
shtukas Jan 13, 2024
e08c526
Towards ZuoraSubscriptionUpdate (3)
shtukas Jan 13, 2024
76473f2
refactor
shtukas Jan 13, 2024
969c3db
Towards ZuoraSubscriptionUpdate (4) introduce IndividualCharge2024
shtukas Jan 13, 2024
17cc2b6
refactor
shtukas Jan 13, 2024
85671d0
refactoring
shtukas Jan 13, 2024
643eef8
Towards ZuoraSubscriptionUpdate (5)
shtukas Jan 13, 2024
c3130be
Towards ZuoraSubscriptionUpdate (6)
shtukas Jan 14, 2024
2a8d6a6
introduce price correction factor for estimation
shtukas Jan 19, 2024
d7c71db
price correction factor for amendments
shtukas Jan 21, 2024
22f9991
Merge branch 'main' into ph-20231216-1541-newspaper-migration
shtukas Jan 21, 2024
e42a73d
remove extra protection
shtukas Jan 21, 2024
61a9d07
remove extra cancellation (was for Côte Brasserie)
shtukas Jan 22, 2024
7937cbb
correct typo (thanks Kelvin)
shtukas Jan 22, 2024
fd74c4e
introduce the newspaper2024migration namespace
shtukas Jan 22, 2024
0d67941
refactoring
shtukas Jan 22, 2024
e19fbb6
refactoring
shtukas Jan 22, 2024
fec8064
better error message
shtukas Jan 22, 2024
d22540a
refactoring
shtukas Jan 22, 2024
aeac2a0
refactor
shtukas Jan 22, 2024
96105f6
remove the fixtures inventory
shtukas Jan 23, 2024
924e9f1
Introduce the util.Date
shtukas Jan 23, 2024
7ed8195
refactoring
shtukas Jan 23, 2024
29dc1b2
refactoring
shtukas Jan 23, 2024
17b2519
refactoring
shtukas Jan 23, 2024
109cdc2
Merge branch 'main' into ph-20231216-1541-newspaper-migration
shtukas Jan 24, 2024
aa881ab
comment upddate
shtukas Jan 28, 2024
55ebf8d
StringObfuscation
shtukas Jan 28, 2024
18f8b9a
improve static data anonymity
shtukas Jan 28, 2024
22a0095
refactoring
shtukas Jan 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import pricemigrationengine.model._
import pricemigrationengine.migrations._
import pricemigrationengine.services._
import zio.{Clock, ZIO}

import java.time.{LocalDate, LocalDateTime, ZoneOffset}
import pricemigrationengine.migrations.newspaper2024migration

/** Carries out price-rise amendments in Zuora.
*/
Expand All @@ -16,7 +16,7 @@ object AmendmentHandler extends CohortHandler {
// TODO: move to config
private val batchSize = 100

private def main(cohortSpec: CohortSpec): ZIO[Logging with CohortTable with Zuora, Failure, HandlerOutput] =
private def main(cohortSpec: CohortSpec): ZIO[Logging with CohortTable with Zuora, Failure, HandlerOutput] = {
for {
catalogue <- Zuora.fetchProductCatalogue
count <- CohortTable
Expand All @@ -25,6 +25,7 @@ object AmendmentHandler extends CohortHandler {
.mapZIO(item => amend(cohortSpec, catalogue, item).tapBoth(Logging.logFailure(item), Logging.logSuccess(item)))
.runCount
} yield HandlerOutput(isComplete = count < batchSize)
}

private def amend(
cohortSpec: CohortSpec,
Expand Down Expand Up @@ -208,6 +209,13 @@ object AmendmentHandler extends CohortHandler {
startDate,
)
)
case Newspaper2024 =>
ZIO.fromEither(
newspaper2024migration.Amendment.subscriptionToZuoraSubscriptionUpdate(
subscriptionBeforeUpdate,
startDate,
)
)
case Legacy =>
ZIO.fromEither(
ZuoraSubscriptionUpdate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package pricemigrationengine.handlers

import pricemigrationengine.migrations.newspaper2024migration
import pricemigrationengine.model.CohortTableFilter._
import pricemigrationengine.model.{CohortSpec, _}
import pricemigrationengine.model._
import pricemigrationengine.services._
import zio.{Clock, IO, Random, ZIO}

Expand Down Expand Up @@ -97,7 +98,19 @@ object EstimationHandler extends CohortHandler {

def datesMax(date1: LocalDate, date2: LocalDate): LocalDate = if (date1.isBefore(date2)) date2 else date1
shtukas marked this conversation as resolved.
Show resolved Hide resolved

def startDateGeneralLowerbound(cohortSpec: CohortSpec, today: LocalDate): LocalDate = {
def startDateGeneralLowerbound(
shtukas marked this conversation as resolved.
Show resolved Hide resolved
cohortSpec: CohortSpec,
today: LocalDate
): LocalDate = {
// The startDateGeneralLowerbound is a function of the cohort spec and the notification min time.
// The cohort spec carries the lowest date we specify there can be a price migration, and the notification min
// time ensures the legally required lead time for customer communication. The max of those two dates is the date
// from which we can realistically perform a price increase. With that said, other policies can apply, for
// instance:
// - The one year policy, which demand that we do not price rise customers during the subscription first year
// - The spread: a mechanism, used for monthlies, by which we do not let a large number of monthlies migrate
// during a single month.

datesMax(
cohortSpec.earliestPriceMigrationStartDate,
today.plusDays(
Expand All @@ -123,9 +136,8 @@ object EstimationHandler extends CohortHandler {
.contains("Month")
}

// In legacy print product cases, we have spread the price rises over 3 months for monthly subscriptions, but
// in the case of membership we want to do this over a single month, hence a value of 1.
// For annual subscriptions we are not applying any spread and defaulting to value 1
// In legacy print product cases, we have spread the price rises over 3 months for monthly subscriptions, this is
// the default behaviour. For annual subscriptions we are not applying any spread and defaulting to value 1.
def decideSpreadPeriod(
subscription: ZuoraSubscription,
invoicePreview: ZuoraInvoiceList,
Expand All @@ -135,6 +147,7 @@ object EstimationHandler extends CohortHandler {
MigrationType(cohortSpec) match {
case Membership2023Monthlies => 1
case Membership2023Annuals => 1
case Newspaper2024 => newspaper2024migration.Estimation.startDateSpreadPeriod(subscription)
case _ => 3
}
} else 1
Expand All @@ -146,14 +159,26 @@ object EstimationHandler extends CohortHandler {
cohortSpec: CohortSpec,
today: LocalDate
): IO[ConfigFailure, LocalDate] = {

// We start by deciding the start date general lower bound, which is determined by the cohort's
// earliestPriceMigrationStartDate and the notification period to this migration
val startDateLowerBound1 = startDateGeneralLowerbound(cohortSpec: CohortSpec, today: LocalDate)
// earliestPriceMigrationStartDate and the notification period to this migration. See comment in
// the body of startDateGeneralLowerbound for details.

// Note that for Newspaper2024 we use that migration own version of this function (due to the unusual scheduling
// nature of that migration), which also takes the subscription.

val startDateLowerBound1 = MigrationType(cohortSpec) match {
case Newspaper2024 =>
newspaper2024migration.Estimation.startDateGeneralLowerbound(cohortSpec, today, subscription)
case _ => startDateGeneralLowerbound(cohortSpec, today)
}

// We now respect the policy of not increasing members during their first year
val startDateLowerBound2 = oneYearPolicy(startDateLowerBound1, subscription)

// Looking up the spread period for this migration
val spreadPeriod = decideSpreadPeriod(subscription, invoicePreview, cohortSpec)

for {
randomFactor <- Random.nextIntBetween(0, spreadPeriod)
} yield startDateLowerBound2.plusMonths(randomFactor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import pricemigrationengine.model.membershipworkflow._
import pricemigrationengine.services._
import zio.{Clock, ZIO}
import com.gu.i18n
import pricemigrationengine.migrations.newspaper2024migration

import java.time.LocalDate
import java.time.format.DateTimeFormatter
Expand All @@ -24,30 +25,28 @@ object NotificationHandler extends CohortHandler {
// of 30 days.

val letterMaxNotificationLeadTime = 49
private val letterMinNotificationLeadTime = 35

// Membership migration
// Notification period: -33 (included) to -31 (excluded) days
private val emailMaxNotificationLeadTime = 33
private val emailMinNotificationLeadTime = 31
// The digital migrations' notification window is from -33 (included) to -31 (excluded)

def maxLeadTime(cohortSpec: CohortSpec): Int = {
MigrationType(cohortSpec) match {
case Membership2023Monthlies => emailMaxNotificationLeadTime
case Membership2023Annuals => emailMaxNotificationLeadTime
case SupporterPlus2023V1V2MA => emailMaxNotificationLeadTime
case DigiSubs2023 => emailMaxNotificationLeadTime
case Legacy => letterMaxNotificationLeadTime
case Membership2023Monthlies => 33
case Membership2023Annuals => 33
case SupporterPlus2023V1V2MA => 33
case DigiSubs2023 => 33
case Newspaper2024 => newspaper2024migration.StaticData.maxLeadTime
case Legacy => 49
}
}

def minLeadTime(cohortSpec: CohortSpec): Int = {
MigrationType(cohortSpec) match {
case Membership2023Monthlies => emailMinNotificationLeadTime
case Membership2023Annuals => emailMinNotificationLeadTime
case SupporterPlus2023V1V2MA => emailMinNotificationLeadTime
case DigiSubs2023 => emailMinNotificationLeadTime
case Legacy => letterMinNotificationLeadTime
case Membership2023Monthlies => 31
case Membership2023Annuals => 31
case SupporterPlus2023V1V2MA => 31
case DigiSubs2023 => 31
case Newspaper2024 => newspaper2024migration.StaticData.minLeadTime
case Legacy => 35
}
}

Expand Down Expand Up @@ -156,6 +155,7 @@ object NotificationHandler extends CohortHandler {
case Membership2023Annuals => true
case SupporterPlus2023V1V2MA => true
case DigiSubs2023 => true
case Newspaper2024 => true
case Legacy => false
}
cappedEstimatedNewPriceWithCurrencySymbol = s"${currencySymbol}${PriceCap(oldPrice, estimatedNewPrice, forceEstimated)}"
Expand Down Expand Up @@ -253,14 +253,19 @@ object NotificationHandler extends CohortHandler {
cohortSpec: CohortSpec,
address: SalesforceAddress
): Either[NotificationHandlerFailure, String] = {
// The country is usually a required field, this came from the old print migrations. It was
// not required for the 2023 digital migrations. Although technically required for
// the 2024 print migration, "United Kingdom" can be substituted for missing values considering
// that we are only delivery in the UK.
MigrationType(cohortSpec) match {
case Membership2023Monthlies =>
requiredField(address.country.fold(Some("United Kingdom"))(Some(_)), "Contact.OtherAddress.country")
case Membership2023Annuals =>
requiredField(address.country.fold(Some("United Kingdom"))(Some(_)), "Contact.OtherAddress.country")
case SupporterPlus2023V1V2MA =>
requiredField(address.country.fold(Some("United Kingdom"))(Some(_)), "Contact.OtherAddress.country")
case _ => requiredField(address.country, "Contact.OtherAddress.country")
case Newspaper2024 => Right(address.country.getOrElse("United Kingdom"))
case _ => requiredField(address.country, "Contact.OtherAddress.country")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ object SalesforcePriceRiseCreationHandler extends CohortHandler {
case Membership2023Annuals => true
case SupporterPlus2023V1V2MA => true
case DigiSubs2023 => true
case Newspaper2024 => true
case Legacy => false
}
SalesforcePriceRise(
Expand All @@ -99,7 +100,7 @@ object SalesforcePriceRiseCreationHandler extends CohortHandler {
}
}

def handle(input: CohortSpec): ZIO[Logging, Failure, HandlerOutput] =
def handle(input: CohortSpec): ZIO[Logging, Failure, HandlerOutput] = {
main(input).provideSome[Logging](
EnvConfig.cohortTable.layer,
EnvConfig.salesforce.layer,
Expand All @@ -109,4 +110,5 @@ object SalesforcePriceRiseCreationHandler extends CohortHandler {
CohortTableLive.impl(input),
SalesforceClientLive.impl
)
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
package pricemigrationengine.migrations
import pricemigrationengine.model.{
AddZuoraRatePlan,
AmendmentDataFailure,
Monthly,
ChargeOverride,
BillingPeriod,
CohortSpec,
Currency,
DigiSubs2023,
Failure,
MigrationType,
Annual,
Quarterly,
PriceData,
RemoveZuoraRatePlan,
ZuoraRatePlan,
ZuoraRatePlanCharge,
ZuoraSubscription,
ZuoraSubscriptionUpdate
}
import pricemigrationengine.model._

import java.time.LocalDate

Expand Down Expand Up @@ -97,6 +78,7 @@ object DigiSubs2023Migration {
case None => Left(AmendmentDataFailure(s"Could not determine a new annual price for currency: ${currency}"))
case Some(price) => Right(price)
}
case SemiAnnual => Left(AmendmentDataFailure(s"There are no defined semi-annual prices for this migration"))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package pricemigrationengine.migrations.newspaper2024migration
import pricemigrationengine.model._
import pricemigrationengine.migrations.newspaper2024migration.StaticData._
import pricemigrationengine.migrations.newspaper2024migration.Estimation._

import java.time.LocalDate

object Amendment {

def subscriptionToNewChargeDistribution2024(subscription: ZuoraSubscription): Option[ChargeDistribution2024] = {
val priceCorrectionFactor = PriceCapping.priceCorrectionFactor(subscription)
for {
data2024 <- Estimation
.subscriptionToSubscriptionData2024(subscription)
.toOption
priceDistribution <- StaticData.priceDistributionLookup(
data2024.productName,
data2024.billingPeriod,
data2024.ratePlanName
)
} yield chargeDistributionMultiplier(priceDistribution, priceCorrectionFactor)
}

def chargeDistributionToChargeOverrides(
distribution: ChargeDistribution2024,
billingPeriod: String
): List[ChargeOverride] = {
List(
distribution.monday,
distribution.tuesday,
distribution.wednesday,
distribution.thursday,
distribution.friday,
distribution.saturday,
distribution.sunday,
distribution.digitalPack,
).flatten.map { individualCharge =>
ChargeOverride(
productRatePlanChargeId = individualCharge.chargeId,
billingPeriod = billingPeriod,
price = individualCharge.Price
)
}
}

def subscriptionToZuoraSubscriptionUpdate(
subscription: ZuoraSubscription,
effectiveDate: LocalDate,
): Either[AmendmentDataFailure, ZuoraSubscriptionUpdate] = {
for {
data2024 <- Estimation.subscriptionToSubscriptionData2024(subscription).left.map(AmendmentDataFailure)
chargeDistribution <- subscriptionToNewChargeDistribution2024(subscription).toRight(AmendmentDataFailure("error"))
} yield ZuoraSubscriptionUpdate(
add = List(
AddZuoraRatePlan(
productRatePlanId = data2024.targetRatePlanId,
contractEffectiveDate = effectiveDate,
chargeOverrides =
chargeDistributionToChargeOverrides(chargeDistribution, BillingPeriod.toString(data2024.billingPeriod))
)
),
remove = List(
RemoveZuoraRatePlan(
ratePlanId = data2024.ratePlan.id,
effectiveDate
)
),
currentTerm = None,
currentTermPeriodType = None
)
}
}
Loading