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);
}
}