killbill-aplcache

invoice: simplify BillingMode calculations Fix various

7/16/2012 8:22:15 PM

Details

diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/BillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/BillingMode.java
index 7700c4e..39c5246 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/BillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/BillingMode.java
@@ -18,6 +18,8 @@ package com.ning.billing.invoice.model;
 
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 
@@ -25,9 +27,6 @@ import com.ning.billing.catalog.api.BillingPeriod;
 
 public interface BillingMode {
 
-    List<RecurringInvoiceItemData> calculateInvoiceItemData(LocalDate startDate, LocalDate endDate, LocalDate targetDate,
-                                                            DateTimeZone accountTimeZone, int billingCycleDay, BillingPeriod billingPeriod) throws InvalidDateSequenceException;
-
-    List<RecurringInvoiceItemData> calculateInvoiceItemData(LocalDate startDate, LocalDate targetDate,
+    List<RecurringInvoiceItemData> calculateInvoiceItemData(LocalDate startDate, @Nullable LocalDate endDate, LocalDate targetDate,
                                                             DateTimeZone accountTimeZone, int billingCycleDay, BillingPeriod billingPeriod) throws InvalidDateSequenceException;
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
index 85685fd..d942628 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
@@ -20,6 +20,8 @@ import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 
@@ -35,14 +37,10 @@ import static com.ning.billing.invoice.generator.InvoiceDateUtils.calculateProRa
 public class InAdvanceBillingMode implements BillingMode {
 
     @Override
-    public List<RecurringInvoiceItemData> calculateInvoiceItemData(final LocalDate startDate, final LocalDate endDate,
+    public List<RecurringInvoiceItemData> calculateInvoiceItemData(final LocalDate startDate, @Nullable final LocalDate endDate,
                                                                    final LocalDate targetDate, final DateTimeZone accountTimeZone,
                                                                    final int billingCycleDayLocal, final BillingPeriod billingPeriod) throws InvalidDateSequenceException {
-        if (endDate == null) {
-            return calculateInvoiceItemData(startDate, targetDate, accountTimeZone, billingCycleDayLocal, billingPeriod);
-        }
-
-        if (endDate.isBefore(startDate)) {
+        if (endDate != null && endDate.isBefore(startDate)) {
             throw new InvalidDateSequenceException();
         }
         if (targetDate.isBefore(startDate)) {
@@ -63,65 +61,32 @@ public class InAdvanceBillingMode implements BillingMode {
         }
 
         // add one item per billing period
-        final LocalDate effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, endDate, billingPeriod);
+        final LocalDate effectiveEndDate;
+        if (endDate != null) {
+            effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, endDate, billingPeriod);
+        } else {
+            effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, billingPeriod);
+        }
         final LocalDate lastBillingCycleDate = calculateLastBillingCycleDateBefore(effectiveEndDate, firstBillingCycleDate, billingCycleDayLocal, billingPeriod);
         final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(firstBillingCycleDate, lastBillingCycleDate, billingPeriod);
         final int numberOfMonthsPerBillingPeriod = billingPeriod.getNumberOfMonths();
 
         for (int i = 0; i < numberOfWholeBillingPeriods; i++) {
             final LocalDate servicePeriodStartDate;
-            if (i == 0) {
+            if (results.size() > 0) {
+                // Make sure the periods align, especially with the pro-ration calculations above
+                servicePeriodStartDate = results.get(results.size() - 1).getEndDate();
+            } else if (i == 0) {
+                // Use the specified start date
                 servicePeriodStartDate = startDate;
             } else {
-                servicePeriodStartDate = firstBillingCycleDate.plusMonths(i * numberOfMonthsPerBillingPeriod);
-            }
-            results.add(new RecurringInvoiceItemData(servicePeriodStartDate,
-                                                     firstBillingCycleDate.plusMonths((i + 1) * numberOfMonthsPerBillingPeriod), BigDecimal.ONE));
-        }
-
-        // check to see if a trailing pro-ration amount is needed
-        if (effectiveEndDate.isAfter(lastBillingCycleDate)) {
-            final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod);
-            if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
-                results.add(new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods));
+                throw new IllegalStateException("We should at least have one invoice item!");
             }
-        }
-        return results;
-    }
 
-    @Override
-    public List<RecurringInvoiceItemData> calculateInvoiceItemData(final LocalDate startDate,
-                                                                   final LocalDate targetDate,
-                                                                   final DateTimeZone accountTimeZone,
-                                                                   final int billingCycleDayLocal,
-                                                                   final BillingPeriod billingPeriod) throws InvalidDateSequenceException {
-        final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
+            // Make sure to align the end date with the BCD
+            final LocalDate servicePeriodEndDate = firstBillingCycleDate.plusMonths((i + 1) * numberOfMonthsPerBillingPeriod);
 
-        if (targetDate.isBefore(startDate)) {
-            // since the target date is before the start date of the event, this should result in no items being generated
-            throw new InvalidDateSequenceException();
-        }
-
-        // beginning from the start date, find the first billing date
-        final LocalDate firstBillingCycleDate = calculateBillingCycleDateOnOrAfter(startDate, accountTimeZone, billingCycleDayLocal);
-
-        // add pro-ration item if needed
-        if (firstBillingCycleDate.isAfter(startDate)) {
-            final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, firstBillingCycleDate, billingPeriod);
-            if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
-                results.add(new RecurringInvoiceItemData(startDate, firstBillingCycleDate, leadingProRationPeriods));
-            }
-        }
-
-        // add one item per billing period
-        final LocalDate effectiveEndDate = calculateEffectiveEndDate(firstBillingCycleDate, targetDate, billingPeriod);
-        final LocalDate lastBillingCycleDate = calculateLastBillingCycleDateBefore(effectiveEndDate, firstBillingCycleDate, billingCycleDayLocal, billingPeriod);
-        final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(firstBillingCycleDate, lastBillingCycleDate, billingPeriod);
-        final int numberOfMonthsPerBillingPeriod = billingPeriod.getNumberOfMonths();
-
-        for (int i = 0; i < numberOfWholeBillingPeriods; i++) {
-            results.add(new RecurringInvoiceItemData(firstBillingCycleDate.plusMonths(i * numberOfMonthsPerBillingPeriod),
-                                                     firstBillingCycleDate.plusMonths((i + 1) * numberOfMonthsPerBillingPeriod), BigDecimal.ONE));
+            results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE));
         }
 
         // check to see if a trailing pro-ration amount is needed
@@ -131,7 +96,6 @@ public class InAdvanceBillingMode implements BillingMode {
                 results.add(new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods));
             }
         }
-
         return results;
     }
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java b/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java
index 2158cfe..7811ec5 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java
@@ -17,6 +17,7 @@
 package com.ning.billing.invoice.model;
 
 import java.math.BigDecimal;
+import java.util.LinkedHashMap;
 import java.util.List;
 
 import org.joda.time.DateTimeZone;
@@ -28,54 +29,110 @@ import com.ning.billing.catalog.api.BillingPeriod;
 
 public class TestInAdvanceBillingMode {
 
+    private static final DateTimeZone TIMEZONE = DateTimeZone.forID("Pacific/Pitcairn");
+    public static final BillingPeriod BILLING_PERIOD = BillingPeriod.MONTHLY;
+
     @Test(groups = "fast")
     public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDay() throws Exception {
-        final InAdvanceBillingMode billingMode = new InAdvanceBillingMode();
         final LocalDate startDate = new LocalDate(2012, 7, 16);
         final LocalDate endDate = new LocalDate(2012, 8, 16);
         final LocalDate targetDate = new LocalDate(2012, 7, 16);
-        final DateTimeZone dateTimeZone = DateTimeZone.forID("Pacific/Pitcairn");
         final int billingCycleDayLocal = 15;
-        final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
-        final LocalDate servicePeriodEndDate = new LocalDate(2012, 8, 15);
 
-        verifyInvoiceItems(billingMode, startDate, endDate, targetDate, dateTimeZone, billingCycleDayLocal, billingPeriod, servicePeriodEndDate);
+        final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+        expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15));
+
+        verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
     }
 
     @Test(groups = "fast")
     public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDay() throws Exception {
-        final InAdvanceBillingMode billingMode = new InAdvanceBillingMode();
         final LocalDate startDate = new LocalDate(2012, 7, 16);
         final LocalDate endDate = new LocalDate(2012, 8, 16);
         final LocalDate targetDate = new LocalDate(2012, 7, 16);
-        final DateTimeZone dateTimeZone = DateTimeZone.forID("Pacific/Pitcairn");
         final int billingCycleDayLocal = 16;
-        final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
-        final LocalDate servicePeriodEndDate = new LocalDate(2012, 8, 16);
 
-        verifyInvoiceItems(billingMode, startDate, endDate, targetDate, dateTimeZone, billingCycleDayLocal, billingPeriod, servicePeriodEndDate);
+        final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+        expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16));
+
+        verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
     }
 
     @Test(groups = "fast")
     public void testCalculateSimpleInvoiceItemWithBCDAfterStartDay() throws Exception {
-        final InAdvanceBillingMode billingMode = new InAdvanceBillingMode();
         final LocalDate startDate = new LocalDate(2012, 7, 16);
         final LocalDate endDate = new LocalDate(2012, 8, 16);
         final LocalDate targetDate = new LocalDate(2012, 7, 16);
-        final DateTimeZone dateTimeZone = DateTimeZone.forID("Pacific/Pitcairn");
         final int billingCycleDayLocal = 17;
-        final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
-        final LocalDate servicePeriodEndDate = new LocalDate(2012, 7, 17);
 
-        verifyInvoiceItems(billingMode, startDate, endDate, targetDate, dateTimeZone, billingCycleDayLocal, billingPeriod, servicePeriodEndDate);
+        final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+        expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17));
+
+        verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+    }
+
+    @Test(groups = "fast")
+    public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDayWithTargetDateIn3Months() throws Exception {
+        final LocalDate startDate = new LocalDate(2012, 7, 16);
+        final LocalDate endDate = null;
+        final LocalDate targetDate = new LocalDate(2012, 10, 16);
+        final int billingCycleDayLocal = 15;
+
+        final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+        expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15));
+        expectedDates.put(new LocalDate(2012, 8, 15), new LocalDate(2012, 9, 15));
+        expectedDates.put(new LocalDate(2012, 9, 15), new LocalDate(2012, 10, 15));
+        expectedDates.put(new LocalDate(2012, 10, 15), new LocalDate(2012, 11, 15));
+
+        verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
     }
 
-    private void verifyInvoiceItems(final InAdvanceBillingMode billingMode, final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate,
-                                    final DateTimeZone dateTimeZone, final int billingCycleDayLocal, final BillingPeriod billingPeriod, final LocalDate servicePeriodEndDate) throws InvalidDateSequenceException {
+    @Test(groups = "fast")
+    public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDayWithTargetDateIn3Months() throws Exception {
+        final LocalDate startDate = new LocalDate(2012, 7, 16);
+        final LocalDate endDate = null;
+        final LocalDate targetDate = new LocalDate(2012, 10, 16);
+        final int billingCycleDayLocal = 16;
+
+        final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+        expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16));
+        expectedDates.put(new LocalDate(2012, 8, 16), new LocalDate(2012, 9, 16));
+        expectedDates.put(new LocalDate(2012, 9, 16), new LocalDate(2012, 10, 16));
+        expectedDates.put(new LocalDate(2012, 10, 16), new LocalDate(2012, 11, 16));
+
+        verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+    }
+
+    @Test(groups = "fast")
+    public void testCalculateSimpleInvoiceItemWithBCDAfterStartDayWithTargetDateIn3Months() throws Exception {
+        final LocalDate startDate = new LocalDate(2012, 7, 16);
+        final LocalDate endDate = null;
+        final LocalDate targetDate = new LocalDate(2012, 10, 16);
+        final int billingCycleDayLocal = 17;
+
+        final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+        expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17));
+        expectedDates.put(new LocalDate(2012, 7, 17), new LocalDate(2012, 8, 17));
+        expectedDates.put(new LocalDate(2012, 8, 17), new LocalDate(2012, 9, 17));
+        expectedDates.put(new LocalDate(2012, 9, 17), new LocalDate(2012, 10, 17));
+
+        verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+    }
+
+    private void verifyInvoiceItems(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate,
+                                    final DateTimeZone dateTimeZone, final int billingCycleDayLocal, final BillingPeriod billingPeriod,
+                                    final LinkedHashMap<LocalDate, LocalDate> expectedDates) throws InvalidDateSequenceException {
+        final InAdvanceBillingMode billingMode = new InAdvanceBillingMode();
+
         final List<RecurringInvoiceItemData> invoiceItems = billingMode.calculateInvoiceItemData(startDate, endDate, targetDate, dateTimeZone, billingCycleDayLocal, billingPeriod);
-        Assert.assertEquals(invoiceItems.size(), 1);
-        Assert.assertEquals(invoiceItems.get(0).getStartDate(), startDate);
-        Assert.assertEquals(invoiceItems.get(0).getEndDate(), servicePeriodEndDate);
-        Assert.assertTrue(invoiceItems.get(0).getNumberOfCycles().compareTo(BigDecimal.ONE) <= 0);
+
+        int i = 0;
+        for (final LocalDate periodStartDate : expectedDates.keySet()) {
+            Assert.assertEquals(invoiceItems.get(i).getStartDate(), periodStartDate);
+            Assert.assertEquals(invoiceItems.get(i).getEndDate(), expectedDates.get(periodStartDate));
+            Assert.assertTrue(invoiceItems.get(0).getNumberOfCycles().compareTo(BigDecimal.ONE) <= 0);
+            i++;
+        }
+        Assert.assertEquals(invoiceItems.size(), i);
     }
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/ProRationTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/tests/ProRationTestBase.java
index e191f92..fd5cf16 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/ProRationTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/ProRationTestBase.java
@@ -75,7 +75,7 @@ public abstract class ProRationTestBase extends InvoicingTestBase {
     }
 
     protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
-        final List<RecurringInvoiceItemData> items = getBillingMode().calculateInvoiceItemData(startDate, targetDate, DateTimeZone.UTC, billingCycleDay, getBillingPeriod());
+        final List<RecurringInvoiceItemData> items = getBillingMode().calculateInvoiceItemData(startDate, null, targetDate, DateTimeZone.UTC, billingCycleDay, getBillingPeriod());
 
         BigDecimal numberOfBillingCycles = ZERO;
         for (final RecurringInvoiceItemData item : items) {