From 0e0fcaa0bc5ac61ed4b8dedf511cb7bd451f1799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 16:49:44 +0100 Subject: [PATCH 01/13] subscription id logging --- .../pricemigrationengine/handlers/AmendmentHandler.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala index 9424037b..bc7099eb 100644 --- a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala +++ b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala @@ -176,10 +176,16 @@ object AmendmentHandler extends CohortHandler { ) } + _ <- Logging.info(s"Amending subscription ${subscriptionBeforeUpdate.subscriptionNumber} with update ${update}") + newSubscriptionId <- Zuora.updateSubscription(subscriptionBeforeUpdate, update) subscriptionAfterUpdate <- fetchSubscription(item) + _ <- Logging.info( + s"[72f444e3] The new subscription id from Zuora.updateSubscription is ${newSubscriptionId}, and the the id from the newly retrieved subscription is ${subscriptionAfterUpdate.id}" + ) + invoicePreviewAfterUpdate <- Zuora.fetchInvoicePreview(subscriptionAfterUpdate.accountId, invoicePreviewTargetDate) From 61b4277884cacccccbf264289a6f8df3043eb183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 16:53:04 +0100 Subject: [PATCH 02/13] Introduce ZuoraAmendmentOrderPayload --- .../model/ZuoraAmendmentOrderPayload.scala | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala diff --git a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala new file mode 100644 index 00000000..a3cd2c52 --- /dev/null +++ b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala @@ -0,0 +1,76 @@ +package pricemigrationengine.model + +import java.time.LocalDate +import upickle.default.{ReadWriter, macroRW} + +// Zuora documentation: https://knowledgecenter.zuora.com/Zuora_Billing/Manage_subscription_transactions/Orders/Order_actions_tutorials/D_Replace_a_product_in_a_subscription + +sealed trait ZuoraAmendmentOrderPayloadOrderAction +object ZuoraAmendmentOrderPayloadOrderAction { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderAction] = macroRW +} + +case class ZuoraAmendmentOrderPayloadOrderActionTriggerDate(name: String, triggerDate: LocalDate) +object ZuoraAmendmentOrderPayloadOrderActionTriggerDate { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionTriggerDate] = macroRW +} + +case class ZuoraAmendmentOrderPayloadOrderActionRemoveProduct(ratePlanId: String) +object ZuoraAmendmentOrderPayloadOrderActionRemoveProduct { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionRemoveProduct] = macroRW +} + +case class ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates: List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate], + removeProduct: ZuoraAmendmentOrderPayloadOrderActionRemoveProduct +) extends ZuoraAmendmentOrderPayloadOrderAction +object ZuoraAmendmentOrderPayloadOrderActionRemove { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionRemove] = macroRW +} + +case class ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId: String, + pricing: Map[String, Map[String, BigDecimal]] +) +object ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride] = macroRW +} + +case class ZuoraAmendmentOrderPayloadOrderActionAddProduct( + productRatePlanId: String, + chargeOverrides: List[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride] +) +object ZuoraAmendmentOrderPayloadOrderActionAddProduct { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAddProduct] = macroRW +} + +case class ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates: List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate], + addProduct: ZuoraAmendmentOrderPayloadOrderActionAddProduct +) extends ZuoraAmendmentOrderPayloadOrderAction +object ZuoraAmendmentOrderPayloadOrderActionAdd { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAdd] = macroRW +} + +case class ZuoraAmendmentOrderPayloadSubscription( + subscriptionNumber: String, + orderActions: List[ZuoraAmendmentOrderPayloadOrderAction] +) +object ZuoraAmendmentOrderPayloadSubscription { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadSubscription] = macroRW +} + +case class ZuoraAmendmentOrderPayloadProcessingOptions(runBilling: Boolean, collectPayment: Boolean) +object ZuoraAmendmentOrderPayloadProcessingOptions { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadProcessingOptions] = macroRW +} + +case class ZuoraAmendmentOrderPayload( + orderDate: LocalDate, + existingAccountNumber: String, + subscriptions: List[ZuoraAmendmentOrderPayloadSubscription], + processingOptions: ZuoraAmendmentOrderPayloadProcessingOptions +) +object ZuoraAmendmentOrderPayload { + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayload] = macroRW +} From 3e707aac723bc2693deec7ca128bf0655725244d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 16:54:51 +0100 Subject: [PATCH 03/13] Introduce SupporterPlus2024Migration.amendmentOrdersPayload --- .../SupporterPlus2024Migration.scala | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala b/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala index 4ae14217..86a11696 100644 --- a/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala +++ b/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala @@ -323,4 +323,127 @@ object SupporterPlus2024Migration { ) } } + + /* +{ + "orderDate": "2024-10-24", + "existingAccountNumber": "A01955911", + "subscriptions": [ + { + "subscriptionNumber": "A-S02019224", + "orderActions": [ + { + "type": "RemoveProduct", + "triggerDates": [ + { + "name": "ContractEffective", + "triggerDate": "2024-11-26" + }, + { + "name": "ServiceActivation", + "triggerDate": "2024-11-26" + }, + { + "name": "CustomerAcceptance", + "triggerDate": "2024-11-26" + } + ], + "removeProduct": { + "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" + } + }, + { + "type": "AddProduct", + "triggerDates": [ + { + "name": "ContractEffective", + "triggerDate": "2024-11-26" + }, + { + "name": "ServiceActivation", + "triggerDate": "2024-11-26" + }, + { + "name": "CustomerAcceptance", + "triggerDate": "2024-11-26" + } + ], + "addProduct": { + "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", + "chargeOverrides": [ + { + "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", + "pricing": { + "recurringFlatFee": { + "listPrice": 15 + } + } + }, + { + "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", + "pricing": { + "recurringFlatFee": { + "listPrice": 0 + } + } + } + ] + } + } + ] + } + ], + "processingOptions": { + "runBilling": false, + "collectPayment": false + } +} + */ + + def amendmentOrdersPayload( + orderDate: LocalDate, + accountNumber: String, + subscriptionNumber: String, + effectDate: LocalDate, + removeRatePlanId: String, + newBaseAmount: BigDecimal, + newContributionAmount: BigDecimal + ): ZuoraAmendmentOrderPayload = { + val triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate("ContractEffective", effectDate), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate("ServiceActivation", effectDate), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate("CustomerAcceptance", effectDate) + ) + val actionRemove = ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates = triggerDates, + removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct(removeRatePlanId) + ) + val actionAdd = ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates = triggerDates, + addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( + productRatePlanId = "8a128ed885fc6ded018602296ace3eb8", + chargeOverrides = List( + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128ed885fc6ded018602296af13eba", + pricing = Map("recurringFlatFee" -> Map("listPrice" -> newBaseAmount)) + ), + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128d7085fc6dec01860234cd075270", + 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 + ) + } } From a16d07c47828ddc161165765073c347c7de9898e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 17:56:33 +0100 Subject: [PATCH 04/13] Introduce Zuora.applyAmendmentOrder --- .../pricemigrationengine/model/Failure.scala | 1 + .../model/ZuoraAmendmentOrderPayload.scala | 9 ++++++ .../pricemigrationengine/services/Zuora.scala | 11 +++++++ .../services/ZuoraLive.scala | 32 +++++++++++++++++-- .../handlers/NotificationHandlerTest.scala | 5 +++ .../service/MockZuora.scala | 7 +++- 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/lambda/src/main/scala/pricemigrationengine/model/Failure.scala b/lambda/src/main/scala/pricemigrationengine/model/Failure.scala index 7d3d5a69..95a5e005 100644 --- a/lambda/src/main/scala/pricemigrationengine/model/Failure.scala +++ b/lambda/src/main/scala/pricemigrationengine/model/Failure.scala @@ -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 diff --git a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala index a3cd2c52..3767ea83 100644 --- a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala +++ b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala @@ -74,3 +74,12 @@ case class ZuoraAmendmentOrderPayload( object ZuoraAmendmentOrderPayload { implicit val rw: ReadWriter[ZuoraAmendmentOrderPayload] = macroRW } + +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 +} diff --git a/lambda/src/main/scala/pricemigrationengine/services/Zuora.scala b/lambda/src/main/scala/pricemigrationengine/services/Zuora.scala index 93b5e2ec..13da922c 100644 --- a/lambda/src/main/scala/pricemigrationengine/services/Zuora.scala +++ b/lambda/src/main/scala/pricemigrationengine/services/Zuora.scala @@ -20,6 +20,11 @@ trait Zuora { update: ZuoraSubscriptionUpdate ): ZIO[Any, ZuoraUpdateFailure, ZuoraSubscriptionId] + def applyAmendmentOrder( + subscription: ZuoraSubscription, + payload: ZuoraAmendmentOrderPayload + ): ZIO[Any, ZuoraOrderFailure, Unit] + def renewSubscription( subscriptionNumber: String, payload: ZuoraRenewOrderPayload @@ -46,6 +51,12 @@ object Zuora { ): ZIO[Zuora, ZuoraUpdateFailure, ZuoraSubscriptionId] = ZIO.environmentWithZIO(_.get.updateSubscription(subscription, update)) + def applyAmendmentOrder( + subscription: ZuoraSubscription, + payload: ZuoraAmendmentOrderPayload + ): ZIO[Zuora, ZuoraOrderFailure, Unit] = + ZIO.environmentWithZIO(_.get.applyAmendmentOrder(subscription, payload)) + def renewSubscription( subscriptionNumber: String, payload: ZuoraRenewOrderPayload diff --git a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala index 7bda82eb..67c2ba85 100644 --- a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala +++ b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala @@ -208,11 +208,37 @@ object ZuoraLive { ) } + override def applyAmendmentOrder( + subscription: ZuoraSubscription, + payload: ZuoraAmendmentOrderPayload + ): ZIO[Any, ZuoraOrderFailure, Unit] = { + post[ZuoraAmendmentOrderResponse]( + path = s"orders", + body = write(payload) + ).foldZIO( + failure = e => + ZIO.fail( + ZuoraOrderFailure( + s"[f8569839] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, reason: ${e.reason}" + ) + ), + success = response => + if (response.success) { + ZIO.succeed(()) + } else { + ZIO.fail( + ZuoraOrderFailure( + s"[bb6f22ef] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, with answer ${response}" + ) + ) + } + ) + } + override def renewSubscription( subscriptionNumber: String, payload: ZuoraRenewOrderPayload ): ZIO[Any, ZuoraRenewalFailure, Unit] = { - post[ZuoraRenewOrderResponse]( path = s"orders", body = write(payload) @@ -220,7 +246,7 @@ object ZuoraLive { failure = e => ZIO.fail( ZuoraRenewalFailure( - s"[06f5bd6f] subscription number: $subscriptionNumber, payload: ${payload}, reason: ${e.reason}" + s"[06f5bd6f] subscription number: ${subscriptionNumber}, payload: ${payload}, reason: ${e.reason}" ) ), success = response => @@ -229,7 +255,7 @@ object ZuoraLive { } else { ZIO.fail( ZuoraRenewalFailure( - s"[bc532694] subscription number: $subscriptionNumber, payload: ${payload}, with answer ${response}" + s"[bc532694] subscription number: ${subscriptionNumber}, payload: ${payload}, with answer ${response}" ) ) } diff --git a/lambda/src/test/scala/pricemigrationengine/handlers/NotificationHandlerTest.scala b/lambda/src/test/scala/pricemigrationengine/handlers/NotificationHandlerTest.scala index 0f68a3d3..b1321205 100644 --- a/lambda/src/test/scala/pricemigrationengine/handlers/NotificationHandlerTest.scala +++ b/lambda/src/test/scala/pricemigrationengine/handlers/NotificationHandlerTest.scala @@ -163,6 +163,11 @@ class NotificationHandlerTest extends munit.FunSuite { update: ZuoraSubscriptionUpdate ): ZIO[Any, ZuoraUpdateFailure, ZuoraSubscriptionId] = ZIO.succeed("ZuoraSubscriptionId") + override def applyAmendmentOrder( + subscription: ZuoraSubscription, + payload: ZuoraAmendmentOrderPayload + ): ZIO[Any, ZuoraOrderFailure, Unit] = ZIO.succeed(()) + override def renewSubscription( subscriptionNumber: String, payload: ZuoraRenewOrderPayload diff --git a/lambda/src/test/scala/pricemigrationengine/service/MockZuora.scala b/lambda/src/test/scala/pricemigrationengine/service/MockZuora.scala index 16e39837..db077955 100644 --- a/lambda/src/test/scala/pricemigrationengine/service/MockZuora.scala +++ b/lambda/src/test/scala/pricemigrationengine/service/MockZuora.scala @@ -15,7 +15,7 @@ object MockZuora extends Mock[Zuora] { object FetchProductCatalogue extends Effect[Unit, ZuoraFetchFailure, ZuoraProductCatalogue] object UpdateSubscription extends Effect[(ZuoraSubscription, ZuoraSubscriptionUpdate), ZuoraUpdateFailure, ZuoraSubscriptionId] - + object ApplyAmendmentOrder extends Effect[(ZuoraSubscription, ZuoraAmendmentOrderPayload), ZuoraOrderFailure, Unit] object RenewSubscription extends Effect[String, ZuoraRenewalFailure, Unit] val compose: URLayer[Proxy, Zuora] = ZLayer.fromZIO(ZIO.service[Proxy].map { proxy => @@ -38,6 +38,11 @@ object MockZuora extends Mock[Zuora] { : ZIO[Any, ZuoraUpdateFailure, ZuoraSubscriptionId] = proxy(UpdateSubscription, subscription, update) + override def applyAmendmentOrder( + subscription: ZuoraSubscription, + payload: ZuoraAmendmentOrderPayload + ): ZIO[Any, ZuoraOrderFailure, Unit] = proxy(ApplyAmendmentOrder, subscription, payload) + override def renewSubscription(subscriptionNumber: String, payload: ZuoraRenewOrderPayload) : ZIO[Any, ZuoraRenewalFailure, Unit] = proxy(RenewSubscription, subscriptionNumber) From 5f74ba54797d701976e6b0ab2817e84a8529684b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 18:23:22 +0100 Subject: [PATCH 05/13] Prepare doAmendment for new Orders API --- .../handlers/AmendmentHandler.scala | 309 +++++++++++++----- 1 file changed, 219 insertions(+), 90 deletions(-) diff --git a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala index bc7099eb..fa5ea83d 100644 --- a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala +++ b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala @@ -106,119 +106,248 @@ object AmendmentHandler extends CohortHandler { item: CohortItem ): ZIO[Zuora with Logging, Failure, SuccessfulAmendmentResult] = { - for { - subscriptionBeforeUpdate <- fetchSubscription(item) + MigrationType(cohortSpec) match { + case SupporterPlus2024 => { + for { + subscriptionBeforeUpdate <- fetchSubscription(item) - startDate <- ZIO.fromOption(item.startDate).orElseFail(DataExtractionFailure(s"No start date in $item")) + startDate <- ZIO.fromOption(item.startDate).orElseFail(DataExtractionFailure(s"No start date in $item")) - oldPrice <- ZIO.fromOption(item.oldPrice).orElseFail(DataExtractionFailure(s"No old price in $item")) + oldPrice <- ZIO.fromOption(item.oldPrice).orElseFail(DataExtractionFailure(s"No old price in $item")) - estimatedNewPrice <- - ZIO - .fromOption(item.estimatedNewPrice) - .orElseFail(DataExtractionFailure(s"No estimated new price in $item")) + estimatedNewPrice <- + ZIO + .fromOption(item.estimatedNewPrice) + .orElseFail(DataExtractionFailure(s"No estimated new price in $item")) - invoicePreviewTargetDate = startDate.plusMonths(13) + invoicePreviewTargetDate = startDate.plusMonths(13) - account <- Zuora.fetchAccount(subscriptionBeforeUpdate.accountNumber, subscriptionBeforeUpdate.subscriptionNumber) + account <- Zuora.fetchAccount( + subscriptionBeforeUpdate.accountNumber, + subscriptionBeforeUpdate.subscriptionNumber + ) - _ <- renewSubscription(subscriptionBeforeUpdate, subscriptionBeforeUpdate.termEndDate, account) + _ <- renewSubscription(subscriptionBeforeUpdate, subscriptionBeforeUpdate.termEndDate, account) - invoicePreviewBeforeUpdate <- - Zuora.fetchInvoicePreview(subscriptionBeforeUpdate.accountId, invoicePreviewTargetDate) + invoicePreviewBeforeUpdate <- + Zuora.fetchInvoicePreview(subscriptionBeforeUpdate.accountId, invoicePreviewTargetDate) - update <- MigrationType(cohortSpec) match { - case DigiSubs2023 => - ZIO.fromEither( - DigiSubs2023Migration.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - ) - ) - case Newspaper2024 => - ZIO.fromEither( - newspaper2024Migration.Amendment.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - ) + update <- MigrationType(cohortSpec) match { + case DigiSubs2023 => + ZIO.fromEither( + DigiSubs2023Migration.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + ) + ) + case Newspaper2024 => + ZIO.fromEither( + newspaper2024Migration.Amendment.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + ) + ) + case GW2024 => + ZIO.fromEither( + GW2024Migration.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + oldPrice, + estimatedNewPrice, + GW2024Migration.priceCap + ) + ) + case SupporterPlus2024 => + ZIO.fromEither( + SupporterPlus2024Migration.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + oldPrice, + estimatedNewPrice, + SupporterPlus2024Migration.priceCap + ) + ) + case Default => + ZIO.fromEither( + ZuoraSubscriptionUpdate + .zuoraUpdate( + account, + catalogue, + subscriptionBeforeUpdate, + invoicePreviewBeforeUpdate, + startDate, + Some(PriceCap.priceCapLegacy(oldPrice, estimatedNewPrice)) + ) + ) + } + + _ <- Logging.info( + s"Amending subscription ${subscriptionBeforeUpdate.subscriptionNumber} with update ${update}" ) - case GW2024 => - ZIO.fromEither( - GW2024Migration.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - oldPrice, - estimatedNewPrice, - GW2024Migration.priceCap - ) + + newSubscriptionId <- Zuora.updateSubscription(subscriptionBeforeUpdate, update) + + subscriptionAfterUpdate <- fetchSubscription(item) + + _ <- Logging.info( + s"[72f444e3] The new subscription id from Zuora.updateSubscription is ${newSubscriptionId}, and the the id from the newly retrieved subscription is ${subscriptionAfterUpdate.id}" ) - case SupporterPlus2024 => - ZIO.fromEither( - SupporterPlus2024Migration.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - oldPrice, - estimatedNewPrice, - SupporterPlus2024Migration.priceCap + + invoicePreviewAfterUpdate <- + Zuora.fetchInvoicePreview(subscriptionAfterUpdate.accountId, invoicePreviewTargetDate) + + newPrice <- + ZIO.fromEither( + AmendmentData.totalChargeAmount( + subscriptionAfterUpdate, + invoicePreviewAfterUpdate, + startDate + ) ) - ) - case Default => - ZIO.fromEither( - ZuoraSubscriptionUpdate - .zuoraUpdate( - account, - catalogue, - subscriptionBeforeUpdate, - invoicePreviewBeforeUpdate, - startDate, - Some(PriceCap.priceCapLegacy(oldPrice, estimatedNewPrice)) + + _ <- + if (shouldPerformFinalPriceCheck(cohortSpec: CohortSpec) && (newPrice > estimatedNewPrice)) { + ZIO.fail( + DataExtractionFailure( + s"[e9054daa] Item ${item} has gone through the amendment step but has failed the final price check. Estimated price was ${estimatedNewPrice}, but the final price was ${newPrice}" + ) ) - ) + } else { + ZIO.succeed(()) + } + + whenDone <- Clock.instant + } yield SuccessfulAmendmentResult( + item.subscriptionName, + startDate, + oldPrice, + newPrice, + estimatedNewPrice, + newSubscriptionId, + whenDone + ) } + case _ => { + for { + subscriptionBeforeUpdate <- fetchSubscription(item) - _ <- Logging.info(s"Amending subscription ${subscriptionBeforeUpdate.subscriptionNumber} with update ${update}") + startDate <- ZIO.fromOption(item.startDate).orElseFail(DataExtractionFailure(s"No start date in $item")) - newSubscriptionId <- Zuora.updateSubscription(subscriptionBeforeUpdate, update) + oldPrice <- ZIO.fromOption(item.oldPrice).orElseFail(DataExtractionFailure(s"No old price in $item")) - subscriptionAfterUpdate <- fetchSubscription(item) + estimatedNewPrice <- + ZIO + .fromOption(item.estimatedNewPrice) + .orElseFail(DataExtractionFailure(s"No estimated new price in $item")) - _ <- Logging.info( - s"[72f444e3] The new subscription id from Zuora.updateSubscription is ${newSubscriptionId}, and the the id from the newly retrieved subscription is ${subscriptionAfterUpdate.id}" - ) + invoicePreviewTargetDate = startDate.plusMonths(13) - invoicePreviewAfterUpdate <- - Zuora.fetchInvoicePreview(subscriptionAfterUpdate.accountId, invoicePreviewTargetDate) + account <- Zuora.fetchAccount( + subscriptionBeforeUpdate.accountNumber, + subscriptionBeforeUpdate.subscriptionNumber + ) + + _ <- renewSubscription(subscriptionBeforeUpdate, subscriptionBeforeUpdate.termEndDate, account) - newPrice <- - ZIO.fromEither( - AmendmentData.totalChargeAmount( - subscriptionAfterUpdate, - invoicePreviewAfterUpdate, - startDate + invoicePreviewBeforeUpdate <- + Zuora.fetchInvoicePreview(subscriptionBeforeUpdate.accountId, invoicePreviewTargetDate) + + update <- MigrationType(cohortSpec) match { + case DigiSubs2023 => + ZIO.fromEither( + DigiSubs2023Migration.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + ) + ) + case Newspaper2024 => + ZIO.fromEither( + newspaper2024Migration.Amendment.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + ) + ) + case GW2024 => + ZIO.fromEither( + GW2024Migration.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + oldPrice, + estimatedNewPrice, + GW2024Migration.priceCap + ) + ) + case SupporterPlus2024 => + ZIO.fromEither( + SupporterPlus2024Migration.zuoraUpdate( + subscriptionBeforeUpdate, + startDate, + oldPrice, + estimatedNewPrice, + SupporterPlus2024Migration.priceCap + ) + ) + case Default => + ZIO.fromEither( + ZuoraSubscriptionUpdate + .zuoraUpdate( + account, + catalogue, + subscriptionBeforeUpdate, + invoicePreviewBeforeUpdate, + startDate, + Some(PriceCap.priceCapLegacy(oldPrice, estimatedNewPrice)) + ) + ) + } + + _ <- Logging.info( + s"Amending subscription ${subscriptionBeforeUpdate.subscriptionNumber} with update ${update}" ) - ) - _ <- - if (shouldPerformFinalPriceCheck(cohortSpec: CohortSpec) && (newPrice > estimatedNewPrice)) { - ZIO.fail( - DataExtractionFailure( - s"[e9054daa] Item ${item} has gone through the amendment step but has failed the final price check. Estimated price was ${estimatedNewPrice}, but the final price was ${newPrice}" - ) + newSubscriptionId <- Zuora.updateSubscription(subscriptionBeforeUpdate, update) + + subscriptionAfterUpdate <- fetchSubscription(item) + + _ <- Logging.info( + s"[72f444e3] The new subscription id from Zuora.updateSubscription is ${newSubscriptionId}, and the the id from the newly retrieved subscription is ${subscriptionAfterUpdate.id}" ) - } else { - ZIO.succeed(()) - } - whenDone <- Clock.instant - } yield SuccessfulAmendmentResult( - item.subscriptionName, - startDate, - oldPrice, - newPrice, - estimatedNewPrice, - newSubscriptionId, - whenDone - ) + invoicePreviewAfterUpdate <- + Zuora.fetchInvoicePreview(subscriptionAfterUpdate.accountId, invoicePreviewTargetDate) + + newPrice <- + ZIO.fromEither( + AmendmentData.totalChargeAmount( + subscriptionAfterUpdate, + invoicePreviewAfterUpdate, + startDate + ) + ) + + _ <- + if (shouldPerformFinalPriceCheck(cohortSpec: CohortSpec) && (newPrice > estimatedNewPrice)) { + ZIO.fail( + DataExtractionFailure( + s"[e9054daa] Item ${item} has gone through the amendment step but has failed the final price check. Estimated price was ${estimatedNewPrice}, but the final price was ${newPrice}" + ) + ) + } else { + ZIO.succeed(()) + } + + whenDone <- Clock.instant + } yield SuccessfulAmendmentResult( + item.subscriptionName, + startDate, + oldPrice, + newPrice, + estimatedNewPrice, + newSubscriptionId, + whenDone + ) + } + } } def handle(input: CohortSpec): ZIO[Logging, Failure, HandlerOutput] = { From b93d8603bc9f5478f7ffc15ea3aa2a02960f70ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 20:48:56 +0100 Subject: [PATCH 06/13] implement call to new orders --- .../handlers/AmendmentHandler.scala | 93 +++------- .../SupporterPlus2024Migration.scala | 129 ++++++------- .../model/ZuoraAmendmentOrderPayload.scala | 172 +++++++++++++++++- .../model/ZuoraRenewOrderPayload.scala | 7 +- 4 files changed, 246 insertions(+), 155 deletions(-) diff --git a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala index fa5ea83d..318e2659 100644 --- a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala +++ b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala @@ -76,10 +76,15 @@ object AmendmentHandler extends CohortHandler { private def renewSubscription( subscription: ZuoraSubscription, - startDate: LocalDate, + effectDate: LocalDate, account: ZuoraAccount ): ZIO[Zuora with Logging, Failure, Unit] = { - val payload = ZuoraRenewOrderPayload(subscription.subscriptionNumber, startDate, account.basicInfo.accountNumber) + val payload = ZuoraRenewOrderPayload( + LocalDate.now(), + subscription.subscriptionNumber, + account.basicInfo.accountNumber, + effectDate + ) for { _ <- Logging.info(s"Renewing subscription ${subscription.subscriptionNumber} with payload $payload") _ <- Zuora.renewSubscription(subscription.subscriptionNumber, payload) @@ -129,70 +134,27 @@ object AmendmentHandler extends CohortHandler { _ <- renewSubscription(subscriptionBeforeUpdate, subscriptionBeforeUpdate.termEndDate, account) - invoicePreviewBeforeUpdate <- - Zuora.fetchInvoicePreview(subscriptionBeforeUpdate.accountId, invoicePreviewTargetDate) - - update <- MigrationType(cohortSpec) match { - case DigiSubs2023 => - ZIO.fromEither( - DigiSubs2023Migration.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - ) - ) - case Newspaper2024 => - ZIO.fromEither( - newspaper2024Migration.Amendment.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - ) - ) - case GW2024 => - ZIO.fromEither( - GW2024Migration.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - oldPrice, - estimatedNewPrice, - GW2024Migration.priceCap - ) - ) - case SupporterPlus2024 => - ZIO.fromEither( - SupporterPlus2024Migration.zuoraUpdate( - subscriptionBeforeUpdate, - startDate, - oldPrice, - estimatedNewPrice, - SupporterPlus2024Migration.priceCap - ) - ) - case Default => - ZIO.fromEither( - ZuoraSubscriptionUpdate - .zuoraUpdate( - account, - catalogue, - subscriptionBeforeUpdate, - invoicePreviewBeforeUpdate, - startDate, - Some(PriceCap.priceCapLegacy(oldPrice, estimatedNewPrice)) - ) - ) - } + 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) - _ <- Logging.info( - s"[72f444e3] The new subscription id from Zuora.updateSubscription is ${newSubscriptionId}, and the the id from the newly retrieved subscription is ${subscriptionAfterUpdate.id}" - ) - invoicePreviewAfterUpdate <- Zuora.fetchInvoicePreview(subscriptionAfterUpdate.accountId, invoicePreviewTargetDate) @@ -205,17 +167,6 @@ object AmendmentHandler extends CohortHandler { ) ) - _ <- - if (shouldPerformFinalPriceCheck(cohortSpec: CohortSpec) && (newPrice > estimatedNewPrice)) { - ZIO.fail( - DataExtractionFailure( - s"[e9054daa] Item ${item} has gone through the amendment step but has failed the final price check. Estimated price was ${estimatedNewPrice}, but the final price was ${newPrice}" - ) - ) - } else { - ZIO.succeed(()) - } - whenDone <- Clock.instant } yield SuccessfulAmendmentResult( item.subscriptionName, @@ -223,7 +174,7 @@ object AmendmentHandler extends CohortHandler { oldPrice, newPrice, estimatedNewPrice, - newSubscriptionId, + subscriptionAfterUpdate.id, whenDone ) } diff --git a/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala b/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala index 86a11696..4817bfb3 100644 --- a/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala +++ b/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala @@ -324,81 +324,12 @@ object SupporterPlus2024Migration { } } - /* -{ - "orderDate": "2024-10-24", - "existingAccountNumber": "A01955911", - "subscriptions": [ - { - "subscriptionNumber": "A-S02019224", - "orderActions": [ - { - "type": "RemoveProduct", - "triggerDates": [ - { - "name": "ContractEffective", - "triggerDate": "2024-11-26" - }, - { - "name": "ServiceActivation", - "triggerDate": "2024-11-26" - }, - { - "name": "CustomerAcceptance", - "triggerDate": "2024-11-26" - } - ], - "removeProduct": { - "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" - } - }, - { - "type": "AddProduct", - "triggerDates": [ - { - "name": "ContractEffective", - "triggerDate": "2024-11-26" - }, - { - "name": "ServiceActivation", - "triggerDate": "2024-11-26" - }, - { - "name": "CustomerAcceptance", - "triggerDate": "2024-11-26" - } - ], - "addProduct": { - "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", - "chargeOverrides": [ - { - "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", - "pricing": { - "recurringFlatFee": { - "listPrice": 15 - } - } - }, - { - "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", - "pricing": { - "recurringFlatFee": { - "listPrice": 0 - } - } - } - ] - } - } - ] - } - ], - "processingOptions": { - "runBilling": false, - "collectPayment": false - } -} - */ + // ------------------------------------------------ + // Orders API Payloads + // ------------------------------------------------ + + // The two function below have same names but different signatures. + // The second one calls the first one. def amendmentOrdersPayload( orderDate: LocalDate, @@ -406,6 +337,9 @@ object SupporterPlus2024Migration { subscriptionNumber: String, effectDate: LocalDate, removeRatePlanId: String, + productRatePlanId: String, + existingBaseProductRatePlanChargeId: String, + existingContributionRatePlanChargeId: String, newBaseAmount: BigDecimal, newContributionAmount: BigDecimal ): ZuoraAmendmentOrderPayload = { @@ -421,14 +355,14 @@ object SupporterPlus2024Migration { val actionAdd = ZuoraAmendmentOrderPayloadOrderActionAdd( triggerDates = triggerDates, addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( - productRatePlanId = "8a128ed885fc6ded018602296ace3eb8", + productRatePlanId = productRatePlanId, chargeOverrides = List( ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( - productRatePlanChargeId = "8a128ed885fc6ded018602296af13eba", + productRatePlanChargeId = existingBaseProductRatePlanChargeId, pricing = Map("recurringFlatFee" -> Map("listPrice" -> newBaseAmount)) ), ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( - productRatePlanChargeId = "8a128d7085fc6dec01860234cd075270", + productRatePlanChargeId = existingContributionRatePlanChargeId, pricing = Map("recurringFlatFee" -> Map("listPrice" -> newContributionAmount)) ) ) @@ -446,4 +380,43 @@ object SupporterPlus2024Migration { 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 + ) + } } diff --git a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala index 3767ea83..5bc9553d 100644 --- a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala +++ b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala @@ -1,7 +1,7 @@ package pricemigrationengine.model import java.time.LocalDate -import upickle.default.{ReadWriter, macroRW} +import upickle.default._ // Zuora documentation: https://knowledgecenter.zuora.com/Zuora_Billing/Manage_subscription_transactions/Orders/Order_actions_tutorials/D_Replace_a_product_in_a_subscription @@ -25,7 +25,73 @@ case class ZuoraAmendmentOrderPayloadOrderActionRemove( removeProduct: ZuoraAmendmentOrderPayloadOrderActionRemoveProduct ) extends ZuoraAmendmentOrderPayloadOrderAction object ZuoraAmendmentOrderPayloadOrderActionRemove { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionRemove] = macroRW + + /* + + We are using a custom JSON serialisation for this class because the JSON serialisation provided by upickle + would produce: + + { + "$type": "ZuoraAmendmentOrderPayloadOrderActionRemove", + "triggerDates": [ + { + "name": "ContractEffective", + "triggerDate": "2024-11-26" + }, + { + "name": "ServiceActivation", + "triggerDate": "2024-11-26" + }, + { + "name": "CustomerAcceptance", + "triggerDate": "2024-11-26" + } + ], + "removeProduct": { + "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" + } + } + + whereas we want: + + { + "type": "RemoveProduct", + "triggerDates": [ + { + "name": "ContractEffective", + "triggerDate": "2024-11-26" + }, + { + "name": "ServiceActivation", + "triggerDate": "2024-11-26" + }, + { + "name": "CustomerAcceptance", + "triggerDate": "2024-11-26" + } + ], + "removeProduct": { + "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" + } + } + + */ + + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionRemove] = { + readwriter[ujson.Value].bimap[ZuoraAmendmentOrderPayloadOrderActionRemove]( + action => + ujson.Obj( + "type" -> ujson.Str("RemoveProduct"), + "triggerDates" -> writeJs(action.triggerDates), + "removeProduct" -> writeJs(action.removeProduct) + ), + json => + ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates = read[List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate]](json("triggerDates")), + removeProduct = read[ZuoraAmendmentOrderPayloadOrderActionRemoveProduct](json("removeProduct")) + ) + ) + } } case class ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( @@ -49,7 +115,107 @@ case class ZuoraAmendmentOrderPayloadOrderActionAdd( addProduct: ZuoraAmendmentOrderPayloadOrderActionAddProduct ) extends ZuoraAmendmentOrderPayloadOrderAction object ZuoraAmendmentOrderPayloadOrderActionAdd { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAdd] = macroRW + /* + + We are using a custom JSON serialisation for this class because the JSON serialisation provided by upickle +would produce: + + { + "$type": "ZuoraAmendmentOrderPayloadOrderActionAdd", + "triggerDates": [ + { + "name": "ContractEffective", + "triggerDate": "2024-11-26" + }, + { + "name": "ServiceActivation", + "triggerDate": "2024-11-26" + }, + { + "name": "CustomerAcceptance", + "triggerDate": "2024-11-26" + } + ], + "addProduct": { + "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", + "chargeOverrides": [ + { + "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", + "pricing": { + "recurringFlatFee": { + "listPrice": 15 + } + } + }, + { + "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", + "pricing": { + "recurringFlatFee": { + "listPrice": 0 + } + } + } + ] + } + } + + whereas we want: + + { + "type": "AddProduct", + "triggerDates": [ + { + "name": "ContractEffective", + "triggerDate": "2024-11-26" + }, + { + "name": "ServiceActivation", + "triggerDate": "2024-11-26" + }, + { + "name": "CustomerAcceptance", + "triggerDate": "2024-11-26" + } + ], + "addProduct": { + "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", + "chargeOverrides": [ + { + "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", + "pricing": { + "recurringFlatFee": { + "listPrice": 15 + } + } + }, + { + "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", + "pricing": { + "recurringFlatFee": { + "listPrice": 0 + } + } + } + ] + } + } + + */ + implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAdd] = { + readwriter[ujson.Value].bimap[ZuoraAmendmentOrderPayloadOrderActionAdd]( + action => + ujson.Obj( + "type" -> ujson.Str("AddProduct"), + "triggerDates" -> writeJs(action.triggerDates), + "addProduct" -> writeJs(action.addProduct) + ), + json => + ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates = read[List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate]](json("triggerDates")), + addProduct = read[ZuoraAmendmentOrderPayloadOrderActionAddProduct](json("addProduct")) + ) + ) + } } case class ZuoraAmendmentOrderPayloadSubscription( diff --git a/lambda/src/main/scala/pricemigrationengine/model/ZuoraRenewOrderPayload.scala b/lambda/src/main/scala/pricemigrationengine/model/ZuoraRenewOrderPayload.scala index 7d921a25..15db5581 100644 --- a/lambda/src/main/scala/pricemigrationengine/model/ZuoraRenewOrderPayload.scala +++ b/lambda/src/main/scala/pricemigrationengine/model/ZuoraRenewOrderPayload.scala @@ -40,9 +40,10 @@ object ZuoraRenewOrderPayload { implicit val rwZuoraRenewOrderPayload: ReadWriter[ZuoraRenewOrderPayload] = macroRW def apply( + orderDate: LocalDate, subscriptionNumber: String, - effectDate: LocalDate, - accountNumber: String + accountNumber: String, + effectDate: LocalDate ): ZuoraRenewOrderPayload = { val triggerDates = List( ZuoraRenewOrderPayloadOrderActionTriggerDate( @@ -76,7 +77,7 @@ object ZuoraRenewOrderPayload { val processingOptions = ZuoraRenewOrderPayloadProcessingOptions(runBilling = false, collectPayment = false) ZuoraRenewOrderPayload( - orderDate = effectDate, + orderDate = orderDate, existingAccountNumber = accountNumber, subscriptions = subscriptions, processingOptions = processingOptions From dadd7c10b2f73dc5d22dae212064ae8272f05fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 20:49:24 +0100 Subject: [PATCH 07/13] tests --- .../SupporterPlus2024MigrationTest.scala | 386 +++++++++++++++++- 1 file changed, 385 insertions(+), 1 deletion(-) diff --git a/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala b/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala index aa31f7a2..b8667b1e 100644 --- a/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala +++ b/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala @@ -1,10 +1,10 @@ package pricemigrationengine.migrations import pricemigrationengine.model._ +import upickle.default._ import java.time.LocalDate import pricemigrationengine.Fixtures -import pricemigrationengine.migrations.SupporterPlus2024Migration class SupporterPlus2024MigrationTest extends munit.FunSuite { @@ -859,4 +859,388 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) ) } + + // ----------------------------------- + // Orders API + + test("amendmentOrderPayload (monthly)") { + val subscription = Fixtures.subscriptionFromJson("Migrations/SupporterPlus2024/monthly/subscription.json") + assertEquals( + SupporterPlus2024Migration.amendmentOrderPayload( + orderDate = LocalDate.of(2024, 10, 25), + accountNumber = "74bff0f2", + subscriptionNumber = subscription.subscriptionNumber, + effectDate = LocalDate.of(2024, 11, 27), + subscription = subscription, + oldPrice = 10, + estimatedNewPrice = 12, + priceCap = 1.27 + ), + Right( + ZuoraAmendmentOrderPayload( + orderDate = LocalDate.of(2024, 10, 25), + existingAccountNumber = "74bff0f2", + subscriptions = List( + ZuoraAmendmentOrderPayloadSubscription( + subscriptionNumber = "SUBSCRIPTION-NUMBER", + orderActions = List( + ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 11, 27) + ) + ), + removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct( + ratePlanId = "8a12908b8dd07f56018de8f4950923b8" + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 11, 27) + ) + ), + addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( + productRatePlanId = "8a128ed885fc6ded018602296ace3eb8", + chargeOverrides = List( + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128ed885fc6ded018602296af13eba", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 12 + ) + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128d7085fc6dec01860234cd075270", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 0.0 + ) + ) + ) + ) + ) + ) + ) + ) + ), + processingOptions = ZuoraAmendmentOrderPayloadProcessingOptions(false, false) + ) + ) + ) + } + + test("amendmentOrderPayload (monthly), with artificial cap") { + val subscription = Fixtures.subscriptionFromJson("Migrations/SupporterPlus2024/monthly/subscription.json") + assertEquals( + SupporterPlus2024Migration.amendmentOrderPayload( + orderDate = LocalDate.of(2024, 10, 25), + accountNumber = "74bff0f2", + subscriptionNumber = subscription.subscriptionNumber, + effectDate = LocalDate.of(2024, 11, 27), + subscription = subscription, + oldPrice = 10, + estimatedNewPrice = 12, + priceCap = 1.1 + ), + Right( + ZuoraAmendmentOrderPayload( + orderDate = LocalDate.of(2024, 10, 25), + existingAccountNumber = "74bff0f2", + subscriptions = List( + ZuoraAmendmentOrderPayloadSubscription( + subscriptionNumber = "SUBSCRIPTION-NUMBER", + orderActions = List( + ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 11, 27) + ) + ), + removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct( + ratePlanId = "8a12908b8dd07f56018de8f4950923b8" + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 11, 27) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 11, 27) + ) + ), + addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( + productRatePlanId = "8a128ed885fc6ded018602296ace3eb8", + chargeOverrides = List( + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128ed885fc6ded018602296af13eba", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 11 + ) + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128d7085fc6dec01860234cd075270", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 0.0 + ) + ) + ) + ) + ) + ) + ) + ) + ), + processingOptions = ZuoraAmendmentOrderPayloadProcessingOptions(false, false) + ) + ) + ) + } + test("amendmentOrderPayload (annual, with capping)") { + val subscription = Fixtures.subscriptionFromJson("Migrations/SupporterPlus2024/annual/subscription.json") + assertEquals( + SupporterPlus2024Migration.amendmentOrderPayload( + orderDate = LocalDate.of(2024, 9, 9), + accountNumber = "d4a7d0af", + subscriptionNumber = subscription.subscriptionNumber, + effectDate = LocalDate.of(2024, 10, 11), + subscription = subscription, + oldPrice = 150, + estimatedNewPrice = 200, + priceCap = 1.27 + ), + Right( + ZuoraAmendmentOrderPayload( + orderDate = LocalDate.of(2024, 9, 9), + existingAccountNumber = "d4a7d0af", + subscriptions = List( + ZuoraAmendmentOrderPayloadSubscription( + subscriptionNumber = "SUBSCRIPTION-NUMBER", + orderActions = List( + ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 10, 11) + ) + ), + removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct( + ratePlanId = "8a12820a8c0ff963018c2504ba045b2f" + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 10, 11) + ) + ), + addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( + productRatePlanId = "8a128ed885fc6ded01860228f77e3d5a", + chargeOverrides = List( + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128ed885fc6ded01860228f7cb3d5f", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 190.50 + ) + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a12892d85fc6df4018602451322287f", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 340.0 + ) + ) + ) + ) + ) + ) + ) + ) + ), + processingOptions = ZuoraAmendmentOrderPayloadProcessingOptions(false, false) + ) + ) + ) + } + test("Correct JSON serialisation for ZuoraAmendmentOrderPayloadOrderActionRemove") { + val data = ZuoraAmendmentOrderPayloadOrderActionRemove( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 10, 11) + ) + ), + removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct( + ratePlanId = "8a12820a8c0ff963018c2504ba045b2f" + ) + ) + assertEquals( + write(data, indent = 2), + """{ + | "type": "RemoveProduct", + | "triggerDates": [ + | { + | "name": "ContractEffective", + | "triggerDate": "2024-10-11" + | }, + | { + | "name": "ServiceActivation", + | "triggerDate": "2024-10-11" + | }, + | { + | "name": "CustomerAcceptance", + | "triggerDate": "2024-10-11" + | } + | ], + | "removeProduct": { + | "ratePlanId": "8a12820a8c0ff963018c2504ba045b2f" + | } + |}""".stripMargin + ) + } + test("Correct JSON serialisation for ZuoraAmendmentOrderPayloadOrderActionAdd") { + val data = ZuoraAmendmentOrderPayloadOrderActionAdd( + triggerDates = List( + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ContractEffective", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "ServiceActivation", + triggerDate = LocalDate.of(2024, 10, 11) + ), + ZuoraAmendmentOrderPayloadOrderActionTriggerDate( + name = "CustomerAcceptance", + triggerDate = LocalDate.of(2024, 10, 11) + ) + ), + addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( + productRatePlanId = "8a128ed885fc6ded01860228f77e3d5a", + chargeOverrides = List( + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a128ed885fc6ded01860228f7cb3d5f", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 190.50 + ) + ) + ), + ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( + productRatePlanChargeId = "8a12892d85fc6df4018602451322287f", + pricing = Map( + "recurringFlatFee" -> Map( + "listPrice" -> 340.0 + ) + ) + ) + ) + ) + ) + assertEquals( + write(data, indent = 2), + """{ + | "type": "AddProduct", + | "triggerDates": [ + | { + | "name": "ContractEffective", + | "triggerDate": "2024-10-11" + | }, + | { + | "name": "ServiceActivation", + | "triggerDate": "2024-10-11" + | }, + | { + | "name": "CustomerAcceptance", + | "triggerDate": "2024-10-11" + | } + | ], + | "addProduct": { + | "productRatePlanId": "8a128ed885fc6ded01860228f77e3d5a", + | "chargeOverrides": [ + | { + | "productRatePlanChargeId": "8a128ed885fc6ded01860228f7cb3d5f", + | "pricing": { + | "recurringFlatFee": { + | "listPrice": 190.5 + | } + | } + | }, + | { + | "productRatePlanChargeId": "8a12892d85fc6df4018602451322287f", + | "pricing": { + | "recurringFlatFee": { + | "listPrice": 340 + | } + | } + | } + | ] + | } + |}""".stripMargin + ) + } } From 9d67ff6a927a3b872a0b4d477667722037fc63ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Fri, 25 Oct 2024 23:13:59 +0100 Subject: [PATCH 08/13] more tests and type refactoring to pass them --- .../SupporterPlus2024Migration.scala | 2 + .../model/ZuoraAmendmentOrderPayload.scala | 192 ++---------------- .../SupporterPlus2024MigrationTest.scala | 105 +++++++++- 3 files changed, 121 insertions(+), 178 deletions(-) diff --git a/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala b/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala index 4817bfb3..c29cc07b 100644 --- a/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala +++ b/lambda/src/main/scala/pricemigrationengine/migrations/SupporterPlus2024Migration.scala @@ -349,10 +349,12 @@ object SupporterPlus2024Migration { ZuoraAmendmentOrderPayloadOrderActionTriggerDate("CustomerAcceptance", effectDate) ) val actionRemove = ZuoraAmendmentOrderPayloadOrderActionRemove( + `type` = "RemoveProduct", triggerDates = triggerDates, removeProduct = ZuoraAmendmentOrderPayloadOrderActionRemoveProduct(removeRatePlanId) ) val actionAdd = ZuoraAmendmentOrderPayloadOrderActionAdd( + `type` = "AddProduct", triggerDates = triggerDates, addProduct = ZuoraAmendmentOrderPayloadOrderActionAddProduct( productRatePlanId = productRatePlanId, diff --git a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala index 5bc9553d..bc113188 100644 --- a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala +++ b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala @@ -7,91 +7,28 @@ import upickle.default._ sealed trait ZuoraAmendmentOrderPayloadOrderAction object ZuoraAmendmentOrderPayloadOrderAction { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderAction] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderAction] = Writer.merge( + macroW[ZuoraAmendmentOrderPayloadOrderActionAdd], + macroW[ZuoraAmendmentOrderPayloadOrderActionRemove] + ) } - case class ZuoraAmendmentOrderPayloadOrderActionTriggerDate(name: String, triggerDate: LocalDate) object ZuoraAmendmentOrderPayloadOrderActionTriggerDate { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionTriggerDate] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionTriggerDate] = macroW } case class ZuoraAmendmentOrderPayloadOrderActionRemoveProduct(ratePlanId: String) object ZuoraAmendmentOrderPayloadOrderActionRemoveProduct { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionRemoveProduct] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionRemoveProduct] = macroW } case class ZuoraAmendmentOrderPayloadOrderActionRemove( + `type`: String, triggerDates: List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate], removeProduct: ZuoraAmendmentOrderPayloadOrderActionRemoveProduct ) extends ZuoraAmendmentOrderPayloadOrderAction object ZuoraAmendmentOrderPayloadOrderActionRemove { - - /* - - We are using a custom JSON serialisation for this class because the JSON serialisation provided by upickle - would produce: - - { - "$type": "ZuoraAmendmentOrderPayloadOrderActionRemove", - "triggerDates": [ - { - "name": "ContractEffective", - "triggerDate": "2024-11-26" - }, - { - "name": "ServiceActivation", - "triggerDate": "2024-11-26" - }, - { - "name": "CustomerAcceptance", - "triggerDate": "2024-11-26" - } - ], - "removeProduct": { - "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" - } - } - - whereas we want: - - { - "type": "RemoveProduct", - "triggerDates": [ - { - "name": "ContractEffective", - "triggerDate": "2024-11-26" - }, - { - "name": "ServiceActivation", - "triggerDate": "2024-11-26" - }, - { - "name": "CustomerAcceptance", - "triggerDate": "2024-11-26" - } - ], - "removeProduct": { - "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" - } - } - - */ - - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionRemove] = { - readwriter[ujson.Value].bimap[ZuoraAmendmentOrderPayloadOrderActionRemove]( - action => - ujson.Obj( - "type" -> ujson.Str("RemoveProduct"), - "triggerDates" -> writeJs(action.triggerDates), - "removeProduct" -> writeJs(action.removeProduct) - ), - json => - ZuoraAmendmentOrderPayloadOrderActionRemove( - triggerDates = read[List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate]](json("triggerDates")), - removeProduct = read[ZuoraAmendmentOrderPayloadOrderActionRemoveProduct](json("removeProduct")) - ) - ) - } + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionRemove] = macroW } case class ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( @@ -99,7 +36,7 @@ case class ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride( pricing: Map[String, Map[String, BigDecimal]] ) object ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride] = macroW } case class ZuoraAmendmentOrderPayloadOrderActionAddProduct( @@ -107,115 +44,16 @@ case class ZuoraAmendmentOrderPayloadOrderActionAddProduct( chargeOverrides: List[ZuoraAmendmentOrderPayloadOrderActionAddProductChargeOverride] ) object ZuoraAmendmentOrderPayloadOrderActionAddProduct { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAddProduct] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionAddProduct] = macroW } case class ZuoraAmendmentOrderPayloadOrderActionAdd( + `type`: String, triggerDates: List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate], addProduct: ZuoraAmendmentOrderPayloadOrderActionAddProduct ) extends ZuoraAmendmentOrderPayloadOrderAction object ZuoraAmendmentOrderPayloadOrderActionAdd { - /* - - We are using a custom JSON serialisation for this class because the JSON serialisation provided by upickle -would produce: - - { - "$type": "ZuoraAmendmentOrderPayloadOrderActionAdd", - "triggerDates": [ - { - "name": "ContractEffective", - "triggerDate": "2024-11-26" - }, - { - "name": "ServiceActivation", - "triggerDate": "2024-11-26" - }, - { - "name": "CustomerAcceptance", - "triggerDate": "2024-11-26" - } - ], - "addProduct": { - "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", - "chargeOverrides": [ - { - "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", - "pricing": { - "recurringFlatFee": { - "listPrice": 15 - } - } - }, - { - "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", - "pricing": { - "recurringFlatFee": { - "listPrice": 0 - } - } - } - ] - } - } - - whereas we want: - - { - "type": "AddProduct", - "triggerDates": [ - { - "name": "ContractEffective", - "triggerDate": "2024-11-26" - }, - { - "name": "ServiceActivation", - "triggerDate": "2024-11-26" - }, - { - "name": "CustomerAcceptance", - "triggerDate": "2024-11-26" - } - ], - "addProduct": { - "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", - "chargeOverrides": [ - { - "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", - "pricing": { - "recurringFlatFee": { - "listPrice": 15 - } - } - }, - { - "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", - "pricing": { - "recurringFlatFee": { - "listPrice": 0 - } - } - } - ] - } - } - - */ - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadOrderActionAdd] = { - readwriter[ujson.Value].bimap[ZuoraAmendmentOrderPayloadOrderActionAdd]( - action => - ujson.Obj( - "type" -> ujson.Str("AddProduct"), - "triggerDates" -> writeJs(action.triggerDates), - "addProduct" -> writeJs(action.addProduct) - ), - json => - ZuoraAmendmentOrderPayloadOrderActionAdd( - triggerDates = read[List[ZuoraAmendmentOrderPayloadOrderActionTriggerDate]](json("triggerDates")), - addProduct = read[ZuoraAmendmentOrderPayloadOrderActionAddProduct](json("addProduct")) - ) - ) - } + implicit val w: Writer[ZuoraAmendmentOrderPayloadOrderActionAdd] = macroW } case class ZuoraAmendmentOrderPayloadSubscription( @@ -223,12 +61,12 @@ case class ZuoraAmendmentOrderPayloadSubscription( orderActions: List[ZuoraAmendmentOrderPayloadOrderAction] ) object ZuoraAmendmentOrderPayloadSubscription { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadSubscription] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadSubscription] = macroW } case class ZuoraAmendmentOrderPayloadProcessingOptions(runBilling: Boolean, collectPayment: Boolean) object ZuoraAmendmentOrderPayloadProcessingOptions { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayloadProcessingOptions] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayloadProcessingOptions] = macroW } case class ZuoraAmendmentOrderPayload( @@ -238,7 +76,7 @@ case class ZuoraAmendmentOrderPayload( processingOptions: ZuoraAmendmentOrderPayloadProcessingOptions ) object ZuoraAmendmentOrderPayload { - implicit val rw: ReadWriter[ZuoraAmendmentOrderPayload] = macroRW + implicit val w: Writer[ZuoraAmendmentOrderPayload] = macroW } case class ZuoraAmendmentOrderResponse( diff --git a/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala b/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala index b8667b1e..b0a95504 100644 --- a/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala +++ b/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala @@ -885,6 +885,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { subscriptionNumber = "SUBSCRIPTION-NUMBER", orderActions = List( ZuoraAmendmentOrderPayloadOrderActionRemove( + `type` = "RemoveProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -904,6 +905,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) ), ZuoraAmendmentOrderPayloadOrderActionAdd( + `type` = "AddProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -948,7 +950,6 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) ) } - test("amendmentOrderPayload (monthly), with artificial cap") { val subscription = Fixtures.subscriptionFromJson("Migrations/SupporterPlus2024/monthly/subscription.json") assertEquals( @@ -971,6 +972,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { subscriptionNumber = "SUBSCRIPTION-NUMBER", orderActions = List( ZuoraAmendmentOrderPayloadOrderActionRemove( + `type` = "RemoveProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -990,6 +992,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) ), ZuoraAmendmentOrderPayloadOrderActionAdd( + `type` = "AddProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -1056,6 +1059,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { subscriptionNumber = "SUBSCRIPTION-NUMBER", orderActions = List( ZuoraAmendmentOrderPayloadOrderActionRemove( + `type` = "RemoveProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -1075,6 +1079,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) ), ZuoraAmendmentOrderPayloadOrderActionAdd( + `type` = "AddProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -1121,6 +1126,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { } test("Correct JSON serialisation for ZuoraAmendmentOrderPayloadOrderActionRemove") { val data = ZuoraAmendmentOrderPayloadOrderActionRemove( + `type` = "RemoveProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -1142,6 +1148,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { assertEquals( write(data, indent = 2), """{ + | "$type": "ZuoraAmendmentOrderPayloadOrderActionRemove", | "type": "RemoveProduct", | "triggerDates": [ | { @@ -1165,6 +1172,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { } test("Correct JSON serialisation for ZuoraAmendmentOrderPayloadOrderActionAdd") { val data = ZuoraAmendmentOrderPayloadOrderActionAdd( + `type` = "AddProduct", triggerDates = List( ZuoraAmendmentOrderPayloadOrderActionTriggerDate( name = "ContractEffective", @@ -1204,6 +1212,7 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { assertEquals( write(data, indent = 2), """{ + | "$type": "ZuoraAmendmentOrderPayloadOrderActionAdd", | "type": "AddProduct", | "triggerDates": [ | { @@ -1243,4 +1252,98 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { |}""".stripMargin ) } + test( + "Correct JSON serialisation for ZuoraAmendmentOrderPayload using SupporterPlus2024Migration.amendmentOrdersPayload" + ) { + val data = SupporterPlus2024Migration.amendmentOrdersPayload( + orderDate = LocalDate.of(2024, 10, 24), + accountNumber = "A01955911", + subscriptionNumber = "A-S02019224", + effectDate = LocalDate.of(2024, 11, 26), + removeRatePlanId = "8a128e208bdd4251018c0d5050970bd9", + productRatePlanId = "8a128ed885fc6ded018602296ace3eb8", + existingBaseProductRatePlanChargeId = "8a128ed885fc6ded018602296af13eba", + existingContributionRatePlanChargeId = "8a128d7085fc6dec01860234cd075270", + newBaseAmount = 15, + newContributionAmount = 0 + ) + assertEquals( + write(data, indent = 2), + """{ + | "orderDate": "2024-10-24", + | "existingAccountNumber": "A01955911", + | "subscriptions": [ + | { + | "subscriptionNumber": "A-S02019224", + | "orderActions": [ + | { + | "$type": "ZuoraAmendmentOrderPayloadOrderActionRemove", + | "type": "RemoveProduct", + | "triggerDates": [ + | { + | "name": "ContractEffective", + | "triggerDate": "2024-11-26" + | }, + | { + | "name": "ServiceActivation", + | "triggerDate": "2024-11-26" + | }, + | { + | "name": "CustomerAcceptance", + | "triggerDate": "2024-11-26" + | } + | ], + | "removeProduct": { + | "ratePlanId": "8a128e208bdd4251018c0d5050970bd9" + | } + | }, + | { + | "$type": "ZuoraAmendmentOrderPayloadOrderActionAdd", + | "type": "AddProduct", + | "triggerDates": [ + | { + | "name": "ContractEffective", + | "triggerDate": "2024-11-26" + | }, + | { + | "name": "ServiceActivation", + | "triggerDate": "2024-11-26" + | }, + | { + | "name": "CustomerAcceptance", + | "triggerDate": "2024-11-26" + | } + | ], + | "addProduct": { + | "productRatePlanId": "8a128ed885fc6ded018602296ace3eb8", + | "chargeOverrides": [ + | { + | "productRatePlanChargeId": "8a128ed885fc6ded018602296af13eba", + | "pricing": { + | "recurringFlatFee": { + | "listPrice": 15 + | } + | } + | }, + | { + | "productRatePlanChargeId": "8a128d7085fc6dec01860234cd075270", + | "pricing": { + | "recurringFlatFee": { + | "listPrice": 0 + | } + | } + | } + | ] + | } + | } + | ] + | } + | ], + | "processingOptions": { + | "runBilling": false, + | "collectPayment": false + | } + |}""".stripMargin + ) + } } From d428cb310423b87c788dbe55cf6c2824b63fb8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Sat, 26 Oct 2024 18:02:35 +0100 Subject: [PATCH 09/13] ZuoraOrderFailure better string --- .../main/scala/pricemigrationengine/services/ZuoraLive.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala index 67c2ba85..c3ed5513 100644 --- a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala +++ b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala @@ -228,7 +228,7 @@ object ZuoraLive { } else { ZIO.fail( ZuoraOrderFailure( - s"[bb6f22ef] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, with answer ${response}" + s"[bb6f22ef] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, serialised payload: ${write(payload)}, with answer ${response}" ) ) } From 27461b6c5f0f9dd84e9de77bb80d920e2038f7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Sat, 26 Oct 2024 20:38:05 +0100 Subject: [PATCH 10/13] type_flush (hack) --- .../pricemigrationengine/services/ZuoraLive.scala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala index c3ed5513..966053a9 100644 --- a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala +++ b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala @@ -212,9 +212,18 @@ object ZuoraLive { subscription: ZuoraSubscription, payload: ZuoraAmendmentOrderPayload ): ZIO[Any, ZuoraOrderFailure, Unit] = { + + def type_flush(str: String): String = { + str + .replace(""""$type":"ZuoraAmendmentOrderPayloadOrderActionAdd",""", "") + .replace(""""$type":"ZuoraAmendmentOrderPayloadOrderActionRemove",""", "") + } + + val body = type_flush(write(payload)) + post[ZuoraAmendmentOrderResponse]( path = s"orders", - body = write(payload) + body = type_flush(write(payload)) ).foldZIO( failure = e => ZIO.fail( @@ -228,7 +237,7 @@ object ZuoraLive { } else { ZIO.fail( ZuoraOrderFailure( - s"[bb6f22ef] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, serialised payload: ${write(payload)}, with answer ${response}" + s"[bb6f22ef] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, serialised payload: ${body}, with answer ${response}" ) ) } From 4436aa153886b3ac9488dcb26902505eabf1b866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Sat, 26 Oct 2024 22:16:01 +0100 Subject: [PATCH 11/13] comments --- .../model/ZuoraAmendmentOrderPayload.scala | 112 +++++++++++++++++- .../services/ZuoraLive.scala | 5 + .../SupporterPlus2024MigrationTest.scala | 18 +++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala index bc113188..28c727e8 100644 --- a/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala +++ b/lambda/src/main/scala/pricemigrationengine/model/ZuoraAmendmentOrderPayload.scala @@ -3,7 +3,117 @@ package pricemigrationengine.model import java.time.LocalDate import upickle.default._ -// Zuora documentation: https://knowledgecenter.zuora.com/Zuora_Billing/Manage_subscription_transactions/Orders/Order_actions_tutorials/D_Replace_a_product_in_a_subscription +/* + 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 { diff --git a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala index 966053a9..1fb199a3 100644 --- a/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala +++ b/lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala @@ -213,6 +213,11 @@ object ZuoraLive { payload: ZuoraAmendmentOrderPayload ): ZIO[Any, ZuoraOrderFailure, Unit] = { + // The existence of type_flush is explained in comment cada56ad. + // This is, for all intent and purpose a hack due to the way upickle deals with sealed traits + // and in a future change we will get rid of it, either by changing JSON library or coming up + // with the correct writers. + def type_flush(str: String): String = { str .replace(""""$type":"ZuoraAmendmentOrderPayloadOrderActionAdd",""", "") diff --git a/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala b/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala index b0a95504..37b75184 100644 --- a/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala +++ b/lambda/src/test/scala/pricemigrationengine/migrations/SupporterPlus2024MigrationTest.scala @@ -1125,6 +1125,12 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) } test("Correct JSON serialisation for ZuoraAmendmentOrderPayloadOrderActionRemove") { + + // Here we are testing the natural serialization of the whole payload, by upickle. + // The purpose is to ensure that the model hierarchy leads to the same essential JSON structure. + // See comment cada56ad for more details on the structure and information about how we sanitize the JSON + // before we send it to Zuora. + val data = ZuoraAmendmentOrderPayloadOrderActionRemove( `type` = "RemoveProduct", triggerDates = List( @@ -1171,6 +1177,12 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { ) } test("Correct JSON serialisation for ZuoraAmendmentOrderPayloadOrderActionAdd") { + + // Here we are testing the natural serialization of the whole payload, by upickle. + // The purpose is to ensure that the model hierarchy leads to the same essential JSON structure. + // See comment cada56ad for more details on the structure and information about how we sanitize the JSON + // before we send it to Zuora. + val data = ZuoraAmendmentOrderPayloadOrderActionAdd( `type` = "AddProduct", triggerDates = List( @@ -1255,6 +1267,12 @@ class SupporterPlus2024MigrationTest extends munit.FunSuite { test( "Correct JSON serialisation for ZuoraAmendmentOrderPayload using SupporterPlus2024Migration.amendmentOrdersPayload" ) { + + // Here we are testing the natural serialization of the whole payload, by upickle. + // The purpose is to ensure that the model hierarchy leads to the same essential JSON structure. + // See comment cada56ad for more details on the structure and information about how we sanitize the JSON + // before we send it to Zuora. + val data = SupporterPlus2024Migration.amendmentOrdersPayload( orderDate = LocalDate.of(2024, 10, 24), accountNumber = "A01955911", From 479caca475348d968c4d42ed945cb498d121ddda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Sun, 27 Oct 2024 08:05:40 +0000 Subject: [PATCH 12/13] remove logging --- .../pricemigrationengine/handlers/AmendmentHandler.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala index 318e2659..b34f3cb5 100644 --- a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala +++ b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala @@ -260,10 +260,6 @@ object AmendmentHandler extends CohortHandler { subscriptionAfterUpdate <- fetchSubscription(item) - _ <- Logging.info( - s"[72f444e3] The new subscription id from Zuora.updateSubscription is ${newSubscriptionId}, and the the id from the newly retrieved subscription is ${subscriptionAfterUpdate.id}" - ) - invoicePreviewAfterUpdate <- Zuora.fetchInvoicePreview(subscriptionAfterUpdate.accountId, invoicePreviewTargetDate) From 773251a6eb91efa1437197d4b3daa4d57262788e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Honor=C3=A9?= Date: Sun, 27 Oct 2024 08:07:43 +0000 Subject: [PATCH 13/13] use subscriptionAfterUpdate.id --- .../scala/pricemigrationengine/handlers/AmendmentHandler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala index b34f3cb5..e11691b5 100644 --- a/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala +++ b/lambda/src/main/scala/pricemigrationengine/handlers/AmendmentHandler.scala @@ -290,7 +290,7 @@ object AmendmentHandler extends CohortHandler { oldPrice, newPrice, estimatedNewPrice, - newSubscriptionId, + subscriptionAfterUpdate.id, whenDone ) }