From 7876ab6f9c66523911b14d5193a6ec8fbc3fdbce Mon Sep 17 00:00:00 2001 From: Mario Galic Date: Thu, 25 Jul 2019 16:30:47 +0100 Subject: [PATCH 1/2] Model chargedThroughDate as NextInvoiceStartDate --- .../HolidayCreditUpdate.scala | 46 +++++----- .../HolidayStopProcess.scala | 7 +- .../NextInvoiceStartDate.scala | 24 ++++++ .../SubscriptionUpdateTest.scala | 85 +++++++++++-------- 4 files changed, 100 insertions(+), 62 deletions(-) create mode 100644 handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala diff --git a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayCreditUpdate.scala b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayCreditUpdate.scala index 2b4e15a91f..890324801c 100644 --- a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayCreditUpdate.scala +++ b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayCreditUpdate.scala @@ -16,36 +16,32 @@ object HolidayCreditUpdate { def apply( config: Config, subscription: Subscription, - stoppedPublicationDate: LocalDate + stoppedPublicationDate: LocalDate, + nextInvoiceStartDate: LocalDate, + maybeExtendedTerm: Option[ExtendedTerm] ): Either[HolidayStopFailure, HolidayCreditUpdate] = { - - subscription - .originalRatePlanCharge - .flatMap(_.chargedThroughDate) - .toRight(HolidayStopFailure("Original rate plan charge has no charged through date. A bill run is needed to fix this.")) - .map { chargedThroughDate => - val extendedTerm = ExtendedTerm(chargedThroughDate, subscription) - HolidayCreditUpdate( - currentTerm = extendedTerm.map(_.length), - currentTermPeriodType = extendedTerm.map(_.unit), - Seq( - Add( - productRatePlanId = config.holidayCreditProductRatePlanId, - contractEffectiveDate = chargedThroughDate, - customerAcceptanceDate = chargedThroughDate, - serviceActivationDate = chargedThroughDate, - chargeOverrides = Seq( - ChargeOverride( - productRatePlanChargeId = config.holidayCreditProductRatePlanChargeId, - HolidayStart__c = stoppedPublicationDate, - HolidayEnd__c = stoppedPublicationDate, - price = subscription.originalRatePlanCharge.map(HolidayCredit(_)).getOrElse(0) - ) + Right( + HolidayCreditUpdate( + currentTerm = maybeExtendedTerm.map(_.length), + currentTermPeriodType = maybeExtendedTerm.map(_.unit), + Seq( + Add( + productRatePlanId = config.holidayCreditProductRatePlanId, + contractEffectiveDate = nextInvoiceStartDate, + customerAcceptanceDate = nextInvoiceStartDate, + serviceActivationDate = nextInvoiceStartDate, + chargeOverrides = Seq( + ChargeOverride( + productRatePlanChargeId = config.holidayCreditProductRatePlanChargeId, + HolidayStart__c = stoppedPublicationDate, + HolidayEnd__c = stoppedPublicationDate, + price = subscription.originalRatePlanCharge.map(HolidayCredit(_)).getOrElse(0) ) ) ) ) - } + ) + ) } } diff --git a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala index eeddf16ced..399430d7a5 100644 --- a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala +++ b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala @@ -51,6 +51,9 @@ object HolidayStopProcess { result.left.map(ProcessResult.fromOverallFailure).merge } + /** + * This is the main business logic + */ def processHolidayStop( config: Config, getSubscription: SubscriptionName => Either[HolidayStopFailure, Subscription], @@ -59,7 +62,9 @@ object HolidayStopProcess { for { subscription <- getSubscription(stop.subscriptionName) _ <- if (subscription.autoRenew) Right(()) else Left(HolidayStopFailure("Cannot currently process non-auto-renewing subscription")) - holidayCreditUpdate <- HolidayCreditUpdate(config, subscription, stop.stoppedPublicationDate) + nextInvoiceStartDate <- NextInvoiceStartDate(subscription) + maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate, subscription) + holidayCreditUpdate <- HolidayCreditUpdate(config, subscription, stop.stoppedPublicationDate, nextInvoiceStartDate, maybeExtendedTerm) _ <- if (subscription.hasHolidayStop(stop)) Right(()) else updateSubscription(subscription, holidayCreditUpdate) updatedSubscription <- getSubscription(stop.subscriptionName) addedCharge <- updatedSubscription.ratePlanCharge(stop).toRight(HolidayStopFailure("Failed to add charge to subscription")) diff --git a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala new file mode 100644 index 0000000000..205d5d5cb7 --- /dev/null +++ b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala @@ -0,0 +1,24 @@ +package com.gu.holidaystopprocessor + +import java.time.LocalDate + +/** + * Invoiced period is defined as [processedThroughDate, chargedThroughDate) meaning + * - from processedThroughDate inclusive + * - to chargedThroughDate exclusive + * + * Hence chargedThroughDate represents the first day of the next invoiced period. For quarterly + * billing period this would be the first day of the next quarter, whilst for annual this would be + * the first day of the next year. + * + * Note chargedThroughDate is an API concept. The UI and the actual invoice use the term 'Service Period' + * where from and to dates are both inclusive. + */ +object NextInvoiceStartDate { + def apply(subscription: Subscription): Either[HolidayStopFailure, LocalDate] = { + subscription + .originalRatePlanCharge + .flatMap(_.chargedThroughDate) + .toRight(HolidayStopFailure("Original rate plan charge has no charged through date. A bill run is needed to fix this.")) + } +} diff --git a/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala b/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala index 30738c1043..06c4b2e712 100644 --- a/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala +++ b/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala @@ -8,16 +8,22 @@ import org.scalatest.{FlatSpec, Matchers} class SubscriptionUpdateTest extends FlatSpec with Matchers { "holidayCreditToAdd" should "generate update correctly" in { + val subscription = Fixtures.mkSubscription( + termStartDate = LocalDate.of(2019, 7, 12), + termEndDate = LocalDate.of(2020, 7, 12), + price = 42.1, + billingPeriod = "Quarter", + chargedThroughDate = Some(LocalDate.of(2019, 9, 12)) + ) + val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate.right.get, subscription) + val update = HolidayCreditUpdate( config, - subscription = Fixtures.mkSubscription( - termStartDate = LocalDate.of(2019, 7, 12), - termEndDate = LocalDate.of(2020, 7, 12), - price = 42.1, - billingPeriod = "Quarter", - chargedThroughDate = Some(LocalDate.of(2019, 9, 12)) - ), - stoppedPublicationDate = LocalDate.of(2019, 5, 18) + subscription = subscription, + stoppedPublicationDate = LocalDate.of(2019, 5, 18), + nextInvoiceStartDate = nextInvoiceStartDate.right.get, + maybeExtendedTerm = maybeExtendedTerm ) update shouldBe Right(HolidayCreditUpdate( currentTerm = None, @@ -42,33 +48,35 @@ class SubscriptionUpdateTest extends FlatSpec with Matchers { } it should "fail to generate an update when there's no charged-through date" in { - val update = HolidayCreditUpdate( - config, - subscription = Fixtures.mkSubscription( - termStartDate = LocalDate.of(2019, 7, 12), - termEndDate = LocalDate.of(2020, 7, 12), - price = 42.1, - billingPeriod = "Quarter", - chargedThroughDate = None - ), - stoppedPublicationDate = LocalDate.of(2019, 5, 18) + val subscription = Fixtures.mkSubscription( + termStartDate = LocalDate.of(2019, 7, 12), + termEndDate = LocalDate.of(2020, 7, 12), + price = 42.1, + billingPeriod = "Quarter", + chargedThroughDate = None ) - update shouldBe Left(HolidayStopFailure( - "Original rate plan charge has no charged through date. A bill run is needed to fix this." + val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + nextInvoiceStartDate shouldBe Left(HolidayStopFailure( + "Original rate plan charge has no charged through date. A bill run is needed to fix this." )) } it should "generate an update with an extended term when charged-through date of subscription is after its term-end date" in { + val subscription = Fixtures.mkSubscription( + termStartDate = LocalDate.of(2019, 7, 23), + termEndDate = LocalDate.of(2020, 7, 23), + price = 150, + billingPeriod = "Annual", + chargedThroughDate = Some(LocalDate.of(2020, 8, 2)) + ) + val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate.right.get, subscription) val update = HolidayCreditUpdate( config, - subscription = Fixtures.mkSubscription( - termStartDate = LocalDate.of(2019, 7, 23), - termEndDate = LocalDate.of(2020, 7, 23), - price = 150, - billingPeriod = "Annual", - chargedThroughDate = Some(LocalDate.of(2020, 8, 2)) - ), - stoppedPublicationDate = LocalDate.of(2019, 8, 6) + subscription = subscription, + stoppedPublicationDate = LocalDate.of(2019, 8, 6), + nextInvoiceStartDate = nextInvoiceStartDate.right.get, + maybeExtendedTerm = maybeExtendedTerm ) update shouldBe Right(HolidayCreditUpdate( currentTerm = Some(376), @@ -91,16 +99,21 @@ class SubscriptionUpdateTest extends FlatSpec with Matchers { } it should "generate an update without an extended term when charged-through date of subscription is on its term-end date" in { + val subscription = Fixtures.mkSubscription( + termStartDate = LocalDate.of(2019, 7, 23), + termEndDate = LocalDate.of(2020, 7, 23), + price = 150, + billingPeriod = "Annual", + chargedThroughDate = Some(LocalDate.of(2020, 7, 23)) + ) + val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate.right.get, subscription) val update = HolidayCreditUpdate( config, - subscription = Fixtures.mkSubscription( - termStartDate = LocalDate.of(2019, 7, 23), - termEndDate = LocalDate.of(2020, 7, 23), - price = 150, - billingPeriod = "Annual", - chargedThroughDate = Some(LocalDate.of(2020, 7, 23)) - ), - stoppedPublicationDate = LocalDate.of(2019, 8, 6) + subscription = subscription, + stoppedPublicationDate = LocalDate.of(2019, 8, 6), + nextInvoiceStartDate = nextInvoiceStartDate.right.get, + maybeExtendedTerm = maybeExtendedTerm ) update shouldBe Right(HolidayCreditUpdate( currentTerm = None, From 2971a5d5545f42d5b0494becddb53bf897cba5f9 Mon Sep 17 00:00:00 2001 From: Mario Galic Date: Fri, 26 Jul 2019 13:56:00 +0100 Subject: [PATCH 2/2] Rename to NextBillingPeriodStartDate --- .../gu/holidaystopprocessor/HolidayStopProcess.scala | 2 +- ...rtDate.scala => NextBillingPeriodStartDate.scala} | 12 +++++++++--- .../SubscriptionUpdateTest.scala | 8 ++++---- 3 files changed, 14 insertions(+), 8 deletions(-) rename handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/{NextInvoiceStartDate.scala => NextBillingPeriodStartDate.scala} (63%) diff --git a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala index 399430d7a5..7e2fd4f064 100644 --- a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala +++ b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/HolidayStopProcess.scala @@ -62,7 +62,7 @@ object HolidayStopProcess { for { subscription <- getSubscription(stop.subscriptionName) _ <- if (subscription.autoRenew) Right(()) else Left(HolidayStopFailure("Cannot currently process non-auto-renewing subscription")) - nextInvoiceStartDate <- NextInvoiceStartDate(subscription) + nextInvoiceStartDate <- NextBillingPeriodStartDate(subscription) maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate, subscription) holidayCreditUpdate <- HolidayCreditUpdate(config, subscription, stop.stoppedPublicationDate, nextInvoiceStartDate, maybeExtendedTerm) _ <- if (subscription.hasHolidayStop(stop)) Right(()) else updateSubscription(subscription, holidayCreditUpdate) diff --git a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextBillingPeriodStartDate.scala similarity index 63% rename from handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala rename to handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextBillingPeriodStartDate.scala index 205d5d5cb7..5595aa372a 100644 --- a/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextInvoiceStartDate.scala +++ b/handlers/holiday-stop-processor/src/main/scala/com/gu/holidaystopprocessor/NextBillingPeriodStartDate.scala @@ -3,18 +3,24 @@ package com.gu.holidaystopprocessor import java.time.LocalDate /** - * Invoiced period is defined as [processedThroughDate, chargedThroughDate) meaning + * Holiday credit is applied to the next invoice on the first day of the next billing period. + * + * 'Invoiced period' or `billing period that has already been invoiced` is defined as + * [processedThroughDate, chargedThroughDate) meaning * - from processedThroughDate inclusive * - to chargedThroughDate exclusive * - * Hence chargedThroughDate represents the first day of the next invoiced period. For quarterly + * Hence chargedThroughDate represents the first day of the next billing period. For quarterly * billing period this would be the first day of the next quarter, whilst for annual this would be * the first day of the next year. * * Note chargedThroughDate is an API concept. The UI and the actual invoice use the term 'Service Period' * where from and to dates are both inclusive. + * + * Note nextBillingPeriodStartDate represents a specific date yyyy-mm-dd unlike billingPeriod (quarterly) + * or billingPeriodStartDay (1st of month). */ -object NextInvoiceStartDate { +object NextBillingPeriodStartDate { def apply(subscription: Subscription): Either[HolidayStopFailure, LocalDate] = { subscription .originalRatePlanCharge diff --git a/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala b/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala index 06c4b2e712..7aea3d7155 100644 --- a/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala +++ b/handlers/holiday-stop-processor/src/test/scala/com/gu/holidaystopprocessor/SubscriptionUpdateTest.scala @@ -15,7 +15,7 @@ class SubscriptionUpdateTest extends FlatSpec with Matchers { billingPeriod = "Quarter", chargedThroughDate = Some(LocalDate.of(2019, 9, 12)) ) - val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val nextInvoiceStartDate = NextBillingPeriodStartDate(subscription) val maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate.right.get, subscription) val update = HolidayCreditUpdate( @@ -55,7 +55,7 @@ class SubscriptionUpdateTest extends FlatSpec with Matchers { billingPeriod = "Quarter", chargedThroughDate = None ) - val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val nextInvoiceStartDate = NextBillingPeriodStartDate(subscription) nextInvoiceStartDate shouldBe Left(HolidayStopFailure( "Original rate plan charge has no charged through date. A bill run is needed to fix this." )) @@ -69,7 +69,7 @@ class SubscriptionUpdateTest extends FlatSpec with Matchers { billingPeriod = "Annual", chargedThroughDate = Some(LocalDate.of(2020, 8, 2)) ) - val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val nextInvoiceStartDate = NextBillingPeriodStartDate(subscription) val maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate.right.get, subscription) val update = HolidayCreditUpdate( config, @@ -106,7 +106,7 @@ class SubscriptionUpdateTest extends FlatSpec with Matchers { billingPeriod = "Annual", chargedThroughDate = Some(LocalDate.of(2020, 7, 23)) ) - val nextInvoiceStartDate = NextInvoiceStartDate(subscription) + val nextInvoiceStartDate = NextBillingPeriodStartDate(subscription) val maybeExtendedTerm = ExtendedTerm(nextInvoiceStartDate.right.get, subscription) val update = HolidayCreditUpdate( config,