Skip to content

Commit

Permalink
Merge branch 'ph-20241128-1330-splus' of https://github.com/guardian/…
Browse files Browse the repository at this point in the history
…price-migration-engine into ph-20241128-1330-splus
  • Loading branch information
shtukas committed Nov 28, 2024
2 parents 6e16895 + e299f02 commit 71fb5ec
Show file tree
Hide file tree
Showing 14 changed files with 919 additions and 16 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/sbt-dependency-graph.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Update Dependency Graph for sbt
on:
push:
branches:
- main
- sbt-dependency-graph-dfe6e8120082f77f
workflow_dispatch:
jobs:
dependency-graph:
runs-on: ubuntu-latest
steps:
- name: Checkout branch
id: checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Install Java
id: java
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.2.0
with:
distribution: corretto
java-version: 17
- name: Install sbt
id: sbt
uses: sbt/setup-sbt@8a071aa780c993c7a204c785d04d3e8eb64ef272 # v1.1.0
- name: Submit dependencies
id: submit
uses: scalacenter/sbt-dependency-submission@64084844d2b0a9b6c3765f33acde2fbe3f5ae7d3 # v3.1.0
- name: Log snapshot for user validation
id: validate
run: cat ${{ steps.submit.outputs.snapshot-json-path }} | jq
permissions:
contents: write
5 changes: 5 additions & 0 deletions lambda/cfn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ Resources:
Type: AWS::S3::Bucket
Properties:
BucketName: !FindInMap [StageMap, !Ref Stage, BucketName]
PublicAccessBlockConfiguration:
BlockPublicAcls: true
IgnorePublicAcls: true
BlockPublicPolicy: true
RestrictPublicBuckets: true

PriceMigrationEngineTableCreateLambda:
Type: AWS::Lambda::Function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,24 @@ object AmendmentHandler extends CohortHandler {

_ <- renewSubscription(subscriptionBeforeUpdate, subscriptionBeforeUpdate.termEndDate, account)

update <- ZIO.fromEither(
SupporterPlus2024Migration.zuoraUpdate(
subscriptionBeforeUpdate,
startDate,
oldPrice,
estimatedNewPrice,
SupporterPlus2024Migration.priceCap
order <- ZIO.fromEither(
SupporterPlus2024Migration.amendmentOrderPayload(
orderDate = LocalDate.now(),
accountNumber = account.basicInfo.accountNumber,
subscriptionNumber = subscriptionBeforeUpdate.subscriptionNumber,
effectDate = startDate,
subscription = subscriptionBeforeUpdate,
oldPrice = oldPrice,
estimatedNewPrice = estimatedNewPrice,
priceCap = SupporterPlus2024Migration.priceCap
)
)

_ <- Logging.info(
s"Amending subscription ${subscriptionBeforeUpdate.subscriptionNumber} with update ${update}"
s"Amending subscription ${subscriptionBeforeUpdate.subscriptionNumber} with order ${order}"
)

newSubscriptionId <- Zuora.updateSubscription(subscriptionBeforeUpdate, update)
_ <- Zuora.applyAmendmentOrder(subscriptionBeforeUpdate, order)

subscriptionAfterUpdate <- fetchSubscription(item)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,102 @@ object SupporterPlus2024Migration {
)
}
}

// ------------------------------------------------
// Orders API Payloads
// ------------------------------------------------

// The two function below have same names but different signatures.
// The second one calls the first one.

def amendmentOrdersPayload(
orderDate: LocalDate,
accountNumber: String,
subscriptionNumber: String,
effectDate: LocalDate,
removeRatePlanId: String,
productRatePlanId: String,
existingBaseProductRatePlanChargeId: String,
existingContributionRatePlanChargeId: String,
newBaseAmount: BigDecimal,
newContributionAmount: BigDecimal
): ZuoraAmendmentOrderPayload = {
val triggerDates = List(
ZuoraAmendmentOrderPayloadOrderActionTriggerDate("ContractEffective", effectDate),
ZuoraAmendmentOrderPayloadOrderActionTriggerDate("ServiceActivation", effectDate),
ZuoraAmendmentOrderPayloadOrderActionTriggerDate("CustomerAcceptance", effectDate)
)
val actionRemove = ZuoraAmendmentOrderPayloadOrderActionRemove(
`type` = "RemoveProduct",
triggerDates = triggerDates,
removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct(removeRatePlanId)
)
val actionAdd = ZuoraAmendmentOrderPayloadOrderActionAdd(
`type` = "AddProduct",
triggerDates = triggerDates,
addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct(
productRatePlanId = productRatePlanId,
chargeOverrides = List(
ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride(
productRatePlanChargeId = existingBaseProductRatePlanChargeId,
pricing = Map("recurringFlatFee" -> Map("listPrice" -> newBaseAmount))
),
ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride(
productRatePlanChargeId = existingContributionRatePlanChargeId,
pricing = Map("recurringFlatFee" -> Map("listPrice" -> newContributionAmount))
)
)
)
)
val orderActions = List(actionRemove, actionAdd)
val subscriptions = List(
ZuoraAmendmentOrderPayloadSubscription(subscriptionNumber, orderActions)
)
val processingOptions = ZuoraAmendmentOrderPayloadProcessingOptions(runBilling = false, collectPayment = false)
ZuoraAmendmentOrderPayload(
orderDate = orderDate,
existingAccountNumber = accountNumber,
subscriptions = subscriptions,
processingOptions = processingOptions
)
}

def amendmentOrderPayload(
orderDate: LocalDate,
accountNumber: String,
subscriptionNumber: String,
effectDate: LocalDate,
subscription: ZuoraSubscription,
oldPrice: BigDecimal,
estimatedNewPrice: BigDecimal,
priceCap: BigDecimal
): Either[Failure, ZuoraAmendmentOrderPayload] = {
for {
existingRatePlan <- getSupporterPlusV2RatePlan(subscription)
existingBaseRatePlanCharge <- getSupporterPlusBaseRatePlanCharge(
subscription.subscriptionNumber,
existingRatePlan
)
existingContributionRatePlanCharge <- getSupporterPlusContributionRatePlanCharge(
subscription.subscriptionNumber,
existingRatePlan
)
existingContributionPrice <- existingContributionRatePlanCharge.price.toRight(
DataExtractionFailure(
s"[e4e702b6] Could not extract existing contribution price for subscription ${subscription.subscriptionNumber}"
)
)
} yield amendmentOrdersPayload(
orderDate = orderDate,
accountNumber = accountNumber,
subscriptionNumber = subscriptionNumber,
effectDate = effectDate,
removeRatePlanId = existingRatePlan.id,
productRatePlanId = existingRatePlan.productRatePlanId,
existingBaseProductRatePlanChargeId = existingBaseRatePlanCharge.productRatePlanChargeId,
existingContributionRatePlanChargeId = existingContributionRatePlanCharge.productRatePlanChargeId,
newBaseAmount = PriceCap.priceCapForNotification(oldPrice, estimatedNewPrice, priceCap),
newContributionAmount = existingContributionPrice
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ case class ZuoraFailure(reason: String) extends Failure
case class ZuoraFetchFailure(reason: String) extends Failure
case class ZuoraUpdateFailure(reason: String) extends Failure
case class ZuoraRenewalFailure(reason: String) extends Failure
case class ZuoraOrderFailure(reason: String) extends Failure

case class CancelledSubscriptionFailure(reason: String) extends Failure
case class ExpiringSubscriptionFailure(reason: String) extends Failure
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package pricemigrationengine.model

import java.time.LocalDate
import upickle.default._

/*
Author: Pascal
Comment id: 4eb4b0a9
This file was written in October 2024 when we introduced Zuora.applyAmendmentOrder with the aim
of eventually decommissioning Zuora.updateSubscription.
The documentation for this Order is available here:
Zuora documentation: https://knowledgecenter.zuora.com/Zuora_Billing/Manage_subscription_transactions/Orders/Order_actions_tutorials/D_Replace_a_product_in_a_subscription
The migration to the new Orders API is currently a work in progress and the choice of the
model hierarchy in this file reflects the fact that we are migrating SupporterPlus2024
from the old API to the new Orders API; in other words migrating the amendment a
SupporterPlus2024 subscription from using Zuora.updateSubscription to using Zuora.applyAmendmentOrder
*/

/*
Author: Pascal
Comment id: cada56ad
The model hierarchy presented in this file captures the making of a ZuoraAmendmentOrderPayload according
to the schema presented here: https://knowledgecenter.zuora.com/Zuora_Billing/Manage_subscription_transactions/Orders/Order_actions_tutorials/D_Replace_a_product_in_a_subscription
One interesting note about the design is using a sealed trait to represent the different types of an Order action,
in the context of replacing a product in a subscription. Indeed, the two actions in that order are:
{
"type": "RemoveProduct",
"triggerDates": [
{
"name": "ContractEffective",
"triggerDate": "2024-11-28"
},
{
"name": "ServiceActivation",
"triggerDate": "2024-11-28"
},
{
"name": "CustomerAcceptance",
"triggerDate": "2024-11-28"
}
],
"removeProduct": {
"ratePlanId": "8a12867e92c341870192c7c46bdb47d6"
}
}
and
{
"type": "AddProduct",
"triggerDates": [
{
"name": "ContractEffective",
"triggerDate": "2024-11-28"
},
{
"name": "ServiceActivation",
"triggerDate": "2024-11-28"
},
{
"name": "CustomerAcceptance",
"triggerDate": "2024-11-28"
}
],
"addProduct": {
"productRatePlanId": "8a128ed885fc6ded018602296ace3eb8",
"chargeOverrides": [
{
"productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba",
"pricing": {
"recurringFlatFee": {
"listPrice": 12
}
}
},
{
"productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270",
"pricing": {
"recurringFlatFee": {
"listPrice": 0
}
}
}
]
}
}
They are respectively modeled as
ZuoraAmendmentOrderPayloadOrderActionRemove
and
ZuoraAmendmentOrderPayloadOrderActionAdd
Because of the way upickle works, we end up with a JSON serialization that has a "$type" field which is a bit annoying.
We can see it in the test data in SupporterPlus2024MigrationTest.
I have tried to submit the Orders payload to Zuora with the "$type" field left inside the JSON but
Zuora errors during the processing. (Technically, it should actually work, but I think that Zuora may be doing payload introspection
beyond the mandatory fields and is error'ing on the "$type" field)
I have tried to remove the "$type" field by providing a custom writer for the serialization of two classes, but
could not get that to work.
In the end the solution I have chosen was to remove the "$type" field from the JSON before sending it to Zuora,
which explains the function type_flush in the ZuoraLive implementation of applyAmendmentOrder
This is, for all intent and purpose a hack and in a future change we may try and get those
custom writers to work to avoid string manipulation in the implementation of applyAmendmentOrder.
*/

sealed trait ZuoraAmendmentOrderPayloadOrderAction
object ZuoraAmendmentOrderPayloadOrderAction {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderAction] = Writer.merge(
macroW[ZuoraAmendmentOrderPayloadOrderActionAdd],
macroW[ZuoraAmendmentOrderPayloadOrderActionRemove]
)
}
case class ZuoraAmendmentOrderPayloadOrderActionTriggerDate(name: String, triggerDate: LocalDate)
object ZuoraAmendmentOrderPayloadOrderActionTriggerDate {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionTriggerDate] = macroW
}

case class ZuoraAmendmentOrderPayloadOrderActionRemoveProduct(ratePlanId: String)
object ZuoraAmendmentOrderPayloadOrderActionRemoveProduct {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionRemoveProduct] = macroW
}

case class ZuoraAmendmentOrderPayloadOrderActionRemove(
`type`: String,
triggerDates: List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate],
removeProduct: ZuoraAmendmentOrderPayloadOrderActionRemoveProduct
) extends ZuoraAmendmentOrderPayloadOrderAction
object ZuoraAmendmentOrderPayloadOrderActionRemove {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionRemove] = macroW
}

case class ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride(
productRatePlanChargeId: String,
pricing: Map[String, Map[String, BigDecimal]]
)
object ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride] = macroW
}

case class ZuoraAmendmentOrderPayloadOrderActionAddProduct(
productRatePlanId: String,
chargeOverrides: List[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride]
)
object ZuoraAmendmentOrderPayloadOrderActionAddProduct {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionAddProduct] = macroW
}

case class ZuoraAmendmentOrderPayloadOrderActionAdd(
`type`: String,
triggerDates: List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate],
addProduct: ZuoraAmendmentOrderPayloadOrderActionAddProduct
) extends ZuoraAmendmentOrderPayloadOrderAction
object ZuoraAmendmentOrderPayloadOrderActionAdd {
implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionAdd] = macroW
}

case class ZuoraAmendmentOrderPayloadSubscription(
subscriptionNumber: String,
orderActions: List[ZuoraAmendmentOrderPayloadOrderAction]
)
object ZuoraAmendmentOrderPayloadSubscription {
implicit val w: Writer[ZuoraAmendmentOrderPayloadSubscription] = macroW
}

case class ZuoraAmendmentOrderPayloadProcessingOptions(runBilling: Boolean, collectPayment: Boolean)
object ZuoraAmendmentOrderPayloadProcessingOptions {
implicit val w: Writer[ZuoraAmendmentOrderPayloadProcessingOptions] = macroW
}

case class ZuoraAmendmentOrderPayload(
orderDate: LocalDate,
existingAccountNumber: String,
subscriptions: List[ZuoraAmendmentOrderPayloadSubscription],
processingOptions: ZuoraAmendmentOrderPayloadProcessingOptions
)
object ZuoraAmendmentOrderPayload {
implicit val w: Writer[ZuoraAmendmentOrderPayload] = macroW
}

case class ZuoraAmendmentOrderResponse(
success: Boolean
// Be careful if you are considering extending this class because the answer's shape
// varies depending on whether the operation was successful or not.
)
object ZuoraAmendmentOrderResponse {
implicit val rw: ReadWriter[ZuoraAmendmentOrderResponse] = macroRW
}
Loading

0 comments on commit 71fb5ec

Please sign in to comment.