Skip to content

Commit 72a6566

Browse files
authored
Merge pull request #1274 from guardian/ph-20251016-1522-43
Membership2025: use standard capping logic at 43%
2 parents 1a270b7 + 3bb86c3 commit 72a6566

4 files changed

Lines changed: 10 additions & 190 deletions

File tree

lambda/src/main/scala/pricemigrationengine/migrations/Membership2025Migration.scala

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,6 @@ object Membership2025Migration {
1212
val maxLeadTime = 35
1313
val minLeadTime = 33
1414

15-
val priceGridStandardOldPrices: Map[(BillingPeriod, String), BigDecimal] = Map(
16-
(Monthly, "GBP") -> BigDecimal(7.0),
17-
(Monthly, "USD") -> BigDecimal(10.0),
18-
(Monthly, "EUR") -> BigDecimal(10.0),
19-
(Monthly, "AUD") -> BigDecimal(15.0),
20-
(Monthly, "CAD") -> BigDecimal(13.0),
21-
(Monthly, "NZD") -> BigDecimal(15.0),
22-
(Monthly, "ROW") -> BigDecimal(10.0),
23-
(Annual, "GBP") -> BigDecimal(80.0),
24-
(Annual, "USD") -> BigDecimal(95.0),
25-
(Annual, "EUR") -> BigDecimal(95.0),
26-
(Annual, "AUD") -> BigDecimal(160.0),
27-
(Annual, "CAD") -> BigDecimal(120.0),
28-
(Annual, "NZD") -> BigDecimal(160.0),
29-
(Annual, "ROW") -> BigDecimal(95.0),
30-
)
31-
3215
val priceGridNewPrices: Map[(BillingPeriod, String), BigDecimal] = Map(
3316
(Monthly, "GBP") -> BigDecimal(10.0),
3417
(Monthly, "USD") -> BigDecimal(13.0),
@@ -48,21 +31,6 @@ object Membership2025Migration {
4831

4932
// -----------------------------------------------------
5033

51-
def subscriptionHasStandardOldPrice(
52-
subscription: ZuoraSubscription,
53-
invoiceList: ZuoraInvoiceList
54-
): Option[Boolean] = {
55-
// This function return Some(true) if the subscription's old price was the standard old price
56-
// and Some(false) otherwise. We return None if the determination could not happen
57-
for {
58-
ratePlan <- SI2025RateplanFromSubAndInvoices.determineRatePlan(subscription, invoiceList)
59-
currency <- SI2025Extractions.determineCurrency(ratePlan)
60-
billingPeriod <- SI2025Extractions.determineBillingPeriod(ratePlan)
61-
oldPrice = SI2025Extractions.determineOldPrice(ratePlan)
62-
standardOldPrice <- priceGridStandardOldPrices.get((billingPeriod, currency))
63-
} yield oldPrice == standardOldPrice
64-
}
65-
6634
def priceData(
6735
subscription: ZuoraSubscription,
6836
invoiceList: ZuoraInvoiceList,

lambda/src/main/scala/pricemigrationengine/model/EstimationHandlerHelper.scala

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ object EstimationHandlerHelper {
1414
case HomeDelivery2025 => Some(1.2)
1515
case Newspaper2025P3 => Some(1.2)
1616
case ProductMigration2025N4 => None
17-
case Membership2025 => Some(1.3) // [1]
17+
case Membership2025 => Some(1.43)
1818
}
19-
// [1] The 30% cap on Membership2025 is going to be applied
20-
// unless the increase came from the price grid.
2119
}
2220

2321
def commsPrice(cohortSpec: CohortSpec, oldPrice: BigDecimal, estimatedNewPriceUncapped: BigDecimal): BigDecimal = {
@@ -27,51 +25,4 @@ object EstimationHandlerHelper {
2725
migrationCapRatio(cohortSpec: CohortSpec).map(ratio => BigDecimal(ratio))
2826
)
2927
}
30-
31-
/*
32-
Date: October 2025
33-
Author: Pascal
34-
35-
As indicated in the mapping above, Membership2025 has special capping requirements.
36-
A price cap of 30% is going to be applied unless the price increase was mandated by the
37-
price grid ?
38-
39-
Q: Why are we doing this ?
40-
A: Some old subs which didn't go through the previous price rise, if we rise them to
41-
the new price, will undergo too high of a rise, and for this we decided to introduce
42-
a cap, a 30% cap. (Which is 10 points higher than our standard 20%). With that said
43-
some price grid increase are higher than 30%, for instance the UK monthly is meant to
44-
go from 7 to 10, which is a 42% increase. To allows these intended rises to apply
45-
we have a special implementation of the comm price computation for Membership2025,
46-
by which we apply a cap unless the sub current (aka: old) price was the starting
47-
price of the price grid.
48-
49-
For this we encoded the old prices of the price grid in the migration module
50-
as well as `subscriptionHasStandardOldPrice` to decide if the current price
51-
of the sub matches the starting price of the marketing price grid. When that is
52-
the case, we do not apply the price cap and just use the new price as intended.
53-
*/
54-
55-
def commsPriceForMembership2025(
56-
cohortSpec: CohortSpec,
57-
oldPrice: BigDecimal,
58-
estimatedNewPriceUncapped: BigDecimal,
59-
subscription: ZuoraSubscription,
60-
invoiceList: ZuoraInvoiceList
61-
): BigDecimal = {
62-
63-
// Note the use of `.get` here on an Option (subscriptionHasStandardOldPrice return
64-
// a Option[Boolean]). The engine will crash if we are in the very pathological
65-
// situation that this determination could not be done.
66-
67-
if (Membership2025Migration.subscriptionHasStandardOldPrice(subscription, invoiceList).get) {
68-
estimatedNewPriceUncapped
69-
} else {
70-
PriceCap.cappedPrice(
71-
oldPrice,
72-
estimatedNewPriceUncapped,
73-
migrationCapRatio(cohortSpec: CohortSpec).map(ratio => BigDecimal(ratio)) // 30%
74-
)
75-
}
76-
}
7728
}

lambda/src/main/scala/pricemigrationengine/model/EstimationResult.scala

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,15 @@ object EstimationResult {
3434
amendmentEffectiveDateLowerBound
3535
)
3636
priceData <- AmendmentData.priceData(account, subscription, cohortSpec, invoiceList)
37-
} yield MigrationType(cohortSpec) match {
38-
case Membership2025 =>
39-
EstimationData(
40-
subscription.subscriptionNumber,
41-
amendmentEffectiveDate,
42-
priceData.currency,
43-
priceData.oldPrice,
44-
priceData.newPrice, // aka: estimatedNewPrice
45-
EstimationHandlerHelper
46-
.commsPriceForMembership2025(cohortSpec, priceData.oldPrice, priceData.newPrice, subscription, invoiceList),
47-
priceData.billingPeriod
48-
)
49-
case _ =>
50-
EstimationData(
51-
subscription.subscriptionNumber,
52-
amendmentEffectiveDate,
53-
priceData.currency,
54-
priceData.oldPrice,
55-
priceData.newPrice, // aka: estimatedNewPrice
56-
EstimationHandlerHelper.commsPrice(cohortSpec, priceData.oldPrice, priceData.newPrice),
57-
priceData.billingPeriod
58-
)
59-
}
37+
} yield EstimationData(
38+
subscription.subscriptionNumber,
39+
amendmentEffectiveDate,
40+
priceData.currency,
41+
priceData.oldPrice,
42+
priceData.newPrice, // aka: estimatedNewPrice
43+
EstimationHandlerHelper.commsPrice(cohortSpec, priceData.oldPrice, priceData.newPrice),
44+
priceData.billingPeriod
45+
)
6046
}
6147
}
6248

lambda/src/test/scala/pricemigrationengine/migrations/Membership2025MigrationTest.scala

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -56,89 +56,4 @@ class Membership2025MigrationTest extends munit.FunSuite {
5656
Right(PriceData("GBP", BigDecimal(5), BigDecimal(10), "Month"))
5757
)
5858
}
59-
60-
test("standard old price detection for sub1") {
61-
val subscription = Fixtures.subscriptionFromJson("Migrations/Membership2025/sub1/subscription.json")
62-
val account = Fixtures.accountFromJson("Migrations/Membership2025/sub1/account.json")
63-
val invoicePreview = Fixtures.invoiceListFromJson("Migrations/Membership2025/sub1/invoice-preview.json")
64-
65-
// The standard of old price for uk (GBP monthlies is 7, so we expect a `true` here)
66-
67-
assertEquals(
68-
Membership2025Migration.subscriptionHasStandardOldPrice(subscription, invoicePreview),
69-
Some(true)
70-
)
71-
}
72-
73-
test("standard old price detection for sub2") {
74-
val subscription = Fixtures.subscriptionFromJson("Migrations/Membership2025/sub2/subscription.json")
75-
val account = Fixtures.accountFromJson("Migrations/Membership2025/sub2/account.json")
76-
val invoicePreview = Fixtures.invoiceListFromJson("Migrations/Membership2025/sub2/invoice-preview.json")
77-
78-
// sub2 is a variation of sub1 with a non standard old price to test the price capping
79-
// The standard of old price for uk (GBP monthlies is 7, so we expect a `false` here)
80-
81-
assertEquals(
82-
Membership2025Migration.subscriptionHasStandardOldPrice(subscription, invoicePreview),
83-
Some(false)
84-
)
85-
}
86-
87-
test("EstimationHandlerHelper.commsPriceForMembership2025") {
88-
val subscription = Fixtures.subscriptionFromJson("Migrations/Membership2025/sub1/subscription.json")
89-
val account = Fixtures.accountFromJson("Migrations/Membership2025/sub1/account.json")
90-
val invoicePreview = Fixtures.invoiceListFromJson("Migrations/Membership2025/sub1/invoice-preview.json")
91-
92-
val cohortSpec = CohortSpec(
93-
"Membership2025",
94-
"none", // irrelevant
95-
LocalDate.of(2025, 10, 16) // irrelevant
96-
)
97-
98-
val commsPrice = EstimationHandlerHelper.commsPriceForMembership2025(
99-
cohortSpec,
100-
BigDecimal(7), // correct old price for sub1
101-
BigDecimal(10), // price grid new price
102-
subscription,
103-
invoicePreview
104-
)
105-
106-
// Here the old price of the sub is the old price of the price grid, so although
107-
// the jump from 7 to 10 is bigger than the 30% price cap of this migration, we
108-
// compute the comms price to be the intended new price of 10
109-
110-
assertEquals(
111-
commsPrice,
112-
BigDecimal(10)
113-
)
114-
}
115-
116-
test("EstimationHandlerHelper.commsPriceForMembership2025") {
117-
val subscription = Fixtures.subscriptionFromJson("Migrations/Membership2025/sub2/subscription.json")
118-
val account = Fixtures.accountFromJson("Migrations/Membership2025/sub2/account.json")
119-
val invoicePreview = Fixtures.invoiceListFromJson("Migrations/Membership2025/sub2/invoice-preview.json")
120-
121-
val cohortSpec = CohortSpec(
122-
"Membership2025",
123-
"none", // irrelevant
124-
LocalDate.of(2025, 10, 16) // irrelevant
125-
)
126-
127-
val commsPrice = EstimationHandlerHelper.commsPriceForMembership2025(
128-
cohortSpec,
129-
BigDecimal(2.5), // correct old price for sub1
130-
BigDecimal(10), // price grid new price
131-
subscription,
132-
invoicePreview
133-
)
134-
135-
// Here the old price of the sub is not the old price of the price grid,
136-
// We apply the 30% cap
137-
138-
assertEquals(
139-
commsPrice,
140-
BigDecimal(3.25) // 2.5 * 1.3
141-
)
142-
}
143-
14459
}

0 commit comments

Comments
 (0)